diff --git a/app/api/routers/rbac_opc.py b/app/api/routers/rbac_opc.py index 408f88f..184344a 100644 --- a/app/api/routers/rbac_opc.py +++ b/app/api/routers/rbac_opc.py @@ -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} diff --git a/app/services/compute_client.py b/app/services/compute_client.py index b917c6a..9be1de2 100644 --- a/app/services/compute_client.py +++ b/app/services/compute_client.py @@ -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( diff --git a/app/services/pineagents_catalog.py b/app/services/pineagents_catalog.py new file mode 100644 index 0000000..30b0fae --- /dev/null +++ b/app/services/pineagents_catalog.py @@ -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]