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(