Files
server-core/app/api/routers/relay.py
T
Pine e761a089b9 feat(compute): 算力中心 余额门控+充值+真实来源数值
- relay 匿名不再回落admin令牌(root无限额)→401或受限RELAY令牌(修无余额可用)
- /admin/compute/user-balance(充值add/subtract/override)+审计; compute_client.adjust_user_quota
- proxy follow_redirects(修gin尾斜杠301/307空数据); ping 返 version+quota_per_unit
2026-08-26 11:59:54 +08:00

98 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""OpenAI 兼容模型中转端点。
PineAgents(8088) 的 ``pineagents`` provider 把 base_url 指向本服务的 ``/v1``
请求进来后丢弃客户端任何 ``Authorization``,统一替换为服务端凭据。
模型调用统一经 **compute-engine**loopback :3000)中转计量(引擎持有渠道 key,
前端不直连);走 ``PINEAGENTS_COMPUTE_ADMIN_TOKEN`` 管理令牌。流式 SSE 原样透传。
"""
from __future__ import annotations
from typing import Any
import httpx
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse
from starlette.background import BackgroundTask
from ... import config
from ...services import compute_catalog
router = APIRouter(prefix="/v1", tags=["relay"])
def _auth_headers(request: Request) -> dict[str, str]:
"""模型调用鉴权:把请求归到「真实用户令牌」,由引擎按该用户余额计量。
- 优先透传客户端 Authorization(用户算力 PAT → compute 归户计量,引擎按其余额判定)。
- 无用户令牌:仅回落受限消费令牌 COMPUTE_RELAY_TOKEN(引擎按该受限用户余额计量)。
- 二者皆无:**拒绝匿名**——绝不回落 COMPUTE_ADMIN_TOKENroot 无限额度),
否则「用户无余额仍能免费使用」。(引擎用户额度耗尽会返回 403,见 billing_session)。
"""
client_auth = request.headers.get("Authorization", "")
if client_auth:
return {"Authorization": client_auth}
if config.COMPUTE_RELAY_TOKEN:
return {"Authorization": f"Bearer {config.COMPUTE_RELAY_TOKEN}"}
raise HTTPException(
status_code=401,
detail="缺少算力令牌:请携带有效的用户算力令牌(Bearer <PAT>)调用",
)
@router.post("/chat/completions")
async def relay_chat_completions(request: Request):
"""把 chat/completions 转发到 compute-engine,流式 SSE 原样透传。"""
try:
body: dict[str, Any] = await request.json()
except Exception as exc: # noqa: BLE001
raise HTTPException(status_code=400, detail="Invalid JSON body") from exc
chat_url = f"{config.COMPUTE_BASE_URL.rstrip('/')}/v1/chat/completions"
stream = bool(body.get("stream", False))
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)
if not stream:
try:
upstream = await client.send(upstream_request)
payload = upstream.json() if upstream.content else None
except Exception: # noqa: BLE001
await client.aclose()
raise HTTPException(status_code=502, detail="Upstream relay failed") from None
await client.aclose()
return JSONResponse(status_code=upstream.status_code, content=payload)
try:
upstream = await client.send(upstream_request, stream=True)
except Exception: # noqa: BLE001
await client.aclose()
raise HTTPException(status_code=502, detail="Upstream relay failed") from None
resp_headers: dict[str, str] = {}
if "content-type" in upstream.headers:
resp_headers["content-type"] = upstream.headers["content-type"]
if "x-request-id" in upstream.headers:
resp_headers["x-request-id"] = upstream.headers["x-request-id"]
return StreamingResponse(
upstream.aiter_raw(),
status_code=upstream.status_code,
media_type="text/event-stream",
headers=resp_headers,
background=BackgroundTask(client.aclose),
)
@router.get("/models")
async def relay_models():
"""返回模型清单(= 算力中心 admin 模型,供 OpenAIProvider.fetch_models 使用)。"""
ids = await compute_catalog.model_ids()
return {
"object": "list",
"data": [{"id": m, "object": "model", "owned_by": "pineagents"} for m in ids],
}