feat(opc): 用户端算力中心(供应商自动对接 PineAgents)

- 新增 /opc/compute/*(require_roles opc_member): base(基础 API 地址)/models(可用模型)/prices(价目)/tokens(我的令牌 列表/创建/删除)
- pineagents_catalog: 供应商自动对接 PineAgents 的模型与价目目录(与 /v1 中继模型一致)
- compute_client: 新增 issue_user_token/list_user_tokens/delete_token(按用户名挂引擎用户)
- 令牌创建经引擎签 PAT, 用户可自服务管理; 引擎数据经 server-core 代理, 前端不直连
This commit is contained in:
Pine
2026-08-25 12:21:15 +08:00
parent 1ca2e38559
commit 6242717e7b
3 changed files with 117 additions and 0 deletions
+57
View File
@@ -14,6 +14,7 @@ from ..schemas.opc import BidRequest, ProfileUpdate, FinanceRecordCreate
from ...rbac import require_roles, write_audit
from ...infrastructure.repositories import Database, new_id, utcnow_iso
from ...infrastructure.models import FinanceRecord
from ...services import compute_client, pineagents_catalog
router = APIRouter(prefix="/opc", tags=["opc"])
@@ -212,3 +213,59 @@ async def opc_affairs(
_u: dict = Depends(require_roles("opc_member")),
):
return await db.portal_pages.get("opc", "affairs") or {"items": []}
# ── 算力中心(用户端,供应商自动对接 PineAgents,令牌自服务)─────────────────
@router.get("/compute/base", summary="算力基础 API 地址")
async def opc_compute_base(
request: Request,
user: dict = Depends(require_roles("opc_member")),
):
"""用户接入地址:经 server-core /v1 中继(OpenAI 兼容)。"""
base = request.base_url.rstrip("/")
return {
"base_url": f"{base}/v1",
"relay_path": "/v1/chat/completions",
"models_path": "/v1/models",
"username": user.get("username"),
"hint": "将 base_url 与下方令牌填入 OpenAI 兼容客户端(如 request_url/base)即可调用本平台模型",
}
@router.get("/compute/models", summary="可用模型(PineAgents 自动对接)")
async def opc_compute_models(_u: dict = Depends(require_roles("opc_member"))):
return {"items": pineagents_catalog.models()}
@router.get("/compute/prices", summary="模型价目")
async def opc_compute_prices(_u: dict = Depends(require_roles("opc_member"))):
return {"items": pineagents_catalog.prices()}
@router.get("/compute/tokens", summary="我的算力令牌")
async def opc_compute_tokens(user: dict = Depends(require_roles("opc_member"))):
"""列出当前用户的引擎消费令牌。"""
try:
data = await compute_client.list_user_tokens(user.get("username"))
except compute_client.ComputeError as exc:
raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc
return {"items": data.get("items", [])}
@router.post("/compute/tokens", summary="创建算力令牌")
async def opc_compute_tokens_create(user: dict = Depends(require_roles("opc_member"))):
"""为当前用户签发一枚消费令牌(供应商自动对接 PineAgents)。"""
try:
token = await compute_client.issue_user_token(user.get("username"), name="OPC 端令牌")
except compute_client.ComputeError as exc:
raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc
return token
@router.delete("/compute/tokens/{token_id}", summary="删除算力令牌")
async def opc_compute_tokens_delete(token_id: int, user: dict = Depends(require_roles("opc_member"))):
try:
await compute_client.delete_token(token_id)
except compute_client.ComputeError as exc:
raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc
return {"ok": True, "token_id": token_id}
+24
View File
@@ -110,6 +110,30 @@ async def get_logs(user_pat: str) -> dict:
return await _request(method="GET", path="/api/log/self", headers=headers)
async def issue_user_token(username: str, name: str = "") -> dict:
"""为某引擎用户签发消费令牌,返回 {id, key, ...}。"""
data = await _request(
method="POST", path="/api/token", headers=_admin_headers(),
json={"username": username, "name": name},
)
return data.get("data") or {}
async def list_user_tokens(username: str) -> dict:
"""列出某引擎用户的消费令牌(按用户名挂到引擎用户)。"""
data = await _request(
method="GET", path=f"/api/token?username={username}", headers=_admin_headers(),
)
return (data.get("data") or {}).get("items") or []
async def delete_token(token_id: int) -> dict:
"""删除引擎令牌。"""
return await _request(
method="DELETE", path=f"/api/token/{token_id}", headers=_admin_headers(),
)
async def sync_user_enabled(username: str, enabled: bool) -> dict:
"""按 username 同步引擎用户启用/禁用(平台用户生命周期自动同步)。"""
return await _request(
+36
View File
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
"""PineAgents 算力目录:用户端算力中心「自动对接的供应商」模型清单与价目。
供应商自动对接 PineAgents —— 用户无需配置上游,直接看到本平台经 server-core /v1 中继
可调用的模型与价格。模型列表与 relay.py 的 _ENGINE_MODEL_IDS 保持一致;价格为 元/百万token。
"""
from __future__ import annotations
# 模型目录(id = OpenAI 兼容模型名,供 /v1/chat/completions 使用)
MODELS: list[dict] = [
{"id": "deepseek-chat", "name": "DeepSeek Chat", "company": "深度求索", "tags": ["对话", "通用"]},
{"id": "deepseek-reasoner", "name": "DeepSeek Reasoner", "company": "深度求索", "tags": ["推理", "深度思考"]},
{"id": "deepseek-v4-flash", "name": "DeepSeek V4 Flash", "company": "深度求索", "tags": ["快速", "低延迟"]},
{"id": "deepseek-v4-pro", "name": "DeepSeek V4 Pro", "company": "深度求索", "tags": ["旗舰", "高性能"]},
]
# 价目:元 / 百万token(输入 / 输出)
PRICES: dict[str, dict] = {
"deepseek-chat": {"input": 0.5, "output": 1.5, "unit": "/百万tokens"},
"deepseek-reasoner": {"input": 1.0, "output": 2.5, "unit": "/百万tokens"},
"deepseek-v4-flash": {"input": 0.3, "output": 1.0, "unit": "/百万tokens"},
"deepseek-v4-pro": {"input": 2.0, "output": 6.0, "unit": "/百万tokens"},
}
def models() -> list[dict]:
"""可用模型清单(加入价格为可选)。"""
out = []
for m in MODELS:
price = PRICES.get(m["id"], {})
out.append({**m, "price": price})
return out
def prices() -> list[dict]:
return [{"id": m["id"], "name": m["name"], **PRICES.get(m["id"], {})} for m in MODELS]