From e5e9d265bfbcc08e74ed26e18d488c8b9f9e1a4c Mon Sep 17 00:00:00 2001 From: Pine Date: Tue, 25 Aug 2026 14:38:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(compute):=20=E5=BD=92=E6=88=B7=E9=97=AD?= =?UTF-8?q?=E7=8E=AF=E2=80=94=E2=80=94relay=20=E9=80=8F=E4=BC=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7PAT=20+=20=E7=94=A8=E6=88=B7=E7=94=A8=E9=87=8F/?= =?UTF-8?q?=E4=BD=99=E9=A2=9D=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - relay /v1 转发优先透传客户端 Authorization(用户算力PAT→compute归户), 无则回落 COMPUTE_RELAY_TOKEN(匿名) - compute_client: user_balance/user_usage(取该用户一枚PAT鉴权查 /api/user/self|self/usage) - rbac_opc 新增 /opc/compute/usage(本月按模型) + /opc/compute/balance --- app/api/routers/rbac_opc.py | 18 ++++++++++++++++++ app/api/routers/relay.py | 12 ++++++++---- app/services/compute_client.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/app/api/routers/rbac_opc.py b/app/api/routers/rbac_opc.py index cc322a7..d54a359 100644 --- a/app/api/routers/rbac_opc.py +++ b/app/api/routers/rbac_opc.py @@ -244,6 +244,24 @@ async def opc_compute_prices(_u: dict = Depends(require_roles("opc_member"))): return {"items": await compute_catalog.prices()} +@router.get("/compute/usage", summary="我的用量(本月按模型)") +async def opc_compute_usage(user: dict = Depends(require_roles("opc_member"))): + try: + data = await compute_client.user_usage(user.get("username")) + except compute_client.ComputeError as exc: + raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc + return data + + +@router.get("/compute/balance", summary="我的算力余额") +async def opc_compute_balance(user: dict = Depends(require_roles("opc_member"))): + try: + data = await compute_client.user_balance(user.get("username")) + except compute_client.ComputeError as exc: + raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc + return data + + @router.get("/compute/tokens", summary="我的算力令牌") async def opc_compute_tokens(user: dict = Depends(require_roles("opc_member"))): """列出当前用户的引擎消费令牌。""" diff --git a/app/api/routers/relay.py b/app/api/routers/relay.py index ae47205..701b7de 100644 --- a/app/api/routers/relay.py +++ b/app/api/routers/relay.py @@ -22,9 +22,13 @@ from ...services import compute_catalog router = APIRouter(prefix="/v1", tags=["relay"]) -def _auth_headers() -> dict[str, str]: - # /v1 转发用引擎能识别的真实消费令牌(PINEAGENTS_COMPUTE_RELAY_TOKEN), - # 与管理的 COMPUTE_ADMIN_TOKEN(内部合成 root) 区分;空令牌不下发,避免 `Bearer ` 非法被 httpx 拒绝。 +def _auth_headers(request: Request | None = None) -> dict[str, str]: + # 优先透传客户端 Authorization(用户算力 PAT → compute 归户计量)。 + # 无则回落服务端消费令牌(COMPUTE_RELAY_TOKEN)→ compute 计匿名账。 + if request is not None: + client_auth = request.headers.get("Authorization", "") + if client_auth: + return {"Authorization": client_auth} token = config.COMPUTE_RELAY_TOKEN or config.COMPUTE_ADMIN_TOKEN if not token: return {} @@ -41,7 +45,7 @@ async def relay_chat_completions(request: Request): chat_url = f"{config.COMPUTE_BASE_URL.rstrip('/')}/v1/chat/completions" stream = bool(body.get("stream", False)) - headers = {**_auth_headers(), "Content-Type": "application/json"} + headers = {**_auth_headers(request), "Content-Type": "application/json"} client = httpx.AsyncClient(timeout=None) upstream_request = client.build_request("POST", chat_url, json=body, headers=headers) diff --git a/app/services/compute_client.py b/app/services/compute_client.py index 9ab895c..1a0a940 100644 --- a/app/services/compute_client.py +++ b/app/services/compute_client.py @@ -136,6 +136,36 @@ async def list_user_tokens(username: str) -> dict: return (data.get("data") or {}).get("items") or [] +async def _user_key(username: str) -> str: + """取该引擎用户一枚有效 PAT(用作 /api/user/self* 鉴权)。""" + items = await list_user_tokens(username) + for it in items or []: + k = it.get("key") or it.get("token") + if k: + return k + return "" + + +async def user_balance(username: str) -> dict: + """用户余额(PAT 鉴权)。""" + key = await _user_key(username) + if not key: + return {"quota": 0, "used_quota": 0, "balance": 0} + data = await _request(method="GET", path="/api/user/self", + headers={"Authorization": f"Bearer {key}"}) + return data.get("data") or {} + + +async def user_usage(username: str) -> dict: + """本月按模型用量/费用(PAT 鉴权)。""" + key = await _user_key(username) + if not key: + return {"items": [], "cost_quota": 0, "input_tokens": 0, "output_tokens": 0} + data = await _request(method="GET", path="/api/user/self/usage", + headers={"Authorization": f"Bearer {key}"}) + return data.get("data") or {} + + async def delete_token(token_id: int) -> dict: """删除引擎令牌。""" return await _request(