feat(compute): 归户闭环——relay 透传用户PAT + 用户用量/余额接口

- 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
This commit is contained in:
Pine
2026-08-25 14:38:23 +08:00
parent 6685d80600
commit e5e9d265bf
3 changed files with 56 additions and 4 deletions
+18
View File
@@ -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"))):
"""列出当前用户的引擎消费令牌。"""
+8 -4
View File
@@ -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)