e761a089b9
- 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
98 lines
3.9 KiB
Python
98 lines
3.9 KiB
Python
# -*- 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_TOKEN(root 无限额度),
|
||
否则「用户无余额仍能免费使用」。(引擎用户额度耗尽会返回 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],
|
||
}
|