From 865f937f4139b0431eb4ac7dacbefa7cf3874470 Mon Sep 17 00:00:00 2001 From: Pine Date: Tue, 25 Aug 2026 13:47:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(compute):=20=E6=8A=8A=20admin=20=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=BC=80=E6=94=BE=E7=BB=99=20OPC=20=E4=B8=8E=20/v1(?= =?UTF-8?q?=E7=AE=97=E5=8A=9B=E4=B8=AD=E5=BF=83=E5=8C=96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - compute_client.list_models(status): 拉取 compute 引擎 admin 模型 - 新增 compute_catalog.py(替代静态 pineagents_catalog): Model行→桌面端形状(id/name/group/价格/能力/actual_model), 仅 status=1 - rbac_opc /opc/compute/models|prices 改读 admin 模型; /v1/models 与之一致 - 删 pineagents_catalog.py(静态, 不再引用) - tests: compute_catalog._map 2 通过 --- app/api/routers/rbac_opc.py | 10 +++--- app/api/routers/relay.py | 17 +++------ app/services/compute_catalog.py | 58 ++++++++++++++++++++++++++++++ app/services/compute_client.py | 9 +++++ app/services/pineagents_catalog.py | 36 ------------------- tests/test_compute_catalog.py | 25 +++++++++++++ 6 files changed, 101 insertions(+), 54 deletions(-) create mode 100644 app/services/compute_catalog.py delete mode 100644 app/services/pineagents_catalog.py create mode 100644 tests/test_compute_catalog.py diff --git a/app/api/routers/rbac_opc.py b/app/api/routers/rbac_opc.py index 184344a..9374a88 100644 --- a/app/api/routers/rbac_opc.py +++ b/app/api/routers/rbac_opc.py @@ -14,7 +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 +from ...services import compute_client, compute_catalog router = APIRouter(prefix="/opc", tags=["opc"]) @@ -232,14 +232,14 @@ async def opc_compute_base( } -@router.get("/compute/models", summary="可用模型(PineAgents 自动对接)") +@router.get("/compute/models", summary="可用模型(算力中心,admin 新增)") async def opc_compute_models(_u: dict = Depends(require_roles("opc_member"))): - return {"items": pineagents_catalog.models()} + return {"items": await compute_catalog.models()} -@router.get("/compute/prices", summary="模型价目") +@router.get("/compute/prices", summary="模型价目(admin 模型)") async def opc_compute_prices(_u: dict = Depends(require_roles("opc_member"))): - return {"items": pineagents_catalog.prices()} + return {"items": await compute_catalog.prices()} @router.get("/compute/tokens", summary="我的算力令牌") diff --git a/app/api/routers/relay.py b/app/api/routers/relay.py index bde00e3..ae47205 100644 --- a/app/api/routers/relay.py +++ b/app/api/routers/relay.py @@ -17,17 +17,10 @@ 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"]) -# 模型清单(供 /v1/models 使用,与 8088 侧保持一致) -_ENGINE_MODEL_IDS = [ - "deepseek-chat", - "deepseek-reasoner", - "deepseek-v4-flash", - "deepseek-v4-pro", -] - def _auth_headers() -> dict[str, str]: # /v1 转发用引擎能识别的真实消费令牌(PINEAGENTS_COMPUTE_RELAY_TOKEN), @@ -86,11 +79,9 @@ async def relay_chat_completions(request: Request): @router.get("/models") async def relay_models(): - """返回模型清单(供 OpenAIProvider.fetch_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 _ENGINE_MODEL_IDS - ], + "data": [{"id": m, "object": "model", "owned_by": "pineagents"} for m in ids], } diff --git a/app/services/compute_catalog.py b/app/services/compute_catalog.py new file mode 100644 index 0000000..92df571 --- /dev/null +++ b/app/services/compute_catalog.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""算力中心模型目录:把 compute 引擎的 admin 模型(models 表,status=1)映射为桌面端可读形状。 + +替代原静态 pineagents_catalog:OPC 桌面端展示/使用的模型 = admin 端新增的模型(算力中心)。 +""" +from __future__ import annotations + +from . import compute_client + + +def _map(row: dict) -> dict: + """Model 行 → 桌面端模型形状。""" + name = row.get("name") or row.get("model_name") or "" + return { + "id": row.get("model_name") or row.get("name") or "", + "model_name": row.get("model_name", ""), + "name": name, + "actual_model": row.get("actual_model") or row.get("model_name", ""), + "company": "", + "group": row.get("group", "default"), + "billing_unit": row.get("billing_unit", "/百万tokens"), + "input_price": row.get("input_price", 0) or 0, + "output_price": row.get("output_price", 0) or 0, + "cache_hit_price": row.get("cache_hit_price", 0) or 0, + "vision_support": bool(row.get("vision_support", False)), + "image_support": bool(row.get("image_support", False)), + "audio_support": bool(row.get("audio_support", False)), + "video_support": bool(row.get("video_support", False)), + "tags": (row.get("tags") or "").split(",") if row.get("tags") else [], + "vendor_id": row.get("vendor_id", 0), + } + + +async def catalog() -> list[dict]: + """从 compute 引擎拉取 admin 模型(status=1)并映射。空/不可达 → 返回空列表。""" + try: + items = await compute_client.list_models(status=1) + except compute_client.ComputeError: + return [] + return [_map(row) for row in items if (row.get("status", 1) == 1)] + + +async def models() -> list[dict]: + return await catalog() + + +async def prices() -> list[dict]: + return [ + {"id": m["id"], "name": m["name"], "input": m["input_price"], + "output": m["output_price"], "cache_hit": m["cache_hit_price"], + "unit": m["billing_unit"]} + for m in await catalog() + ] + + +async def model_ids() -> list[str]: + """供 /v1/models 使用:admin 模型的对外 id。""" + return [m["id"] for m in await catalog() if m["id"]] diff --git a/app/services/compute_client.py b/app/services/compute_client.py index 9be1de2..9ab895c 100644 --- a/app/services/compute_client.py +++ b/app/services/compute_client.py @@ -119,6 +119,15 @@ async def issue_user_token(username: str, name: str = "") -> dict: return data.get("data") or {} +async def list_models(status: int = 1) -> list[dict]: + """列出引擎 admin 模型(models 表)。返回分页 items 列表。""" + data = await _request( + method="GET", path=f"/api/models?status={status}&p=1&page_size=100", + headers=_admin_headers(), + ) + return (data.get("data") or {}).get("items") or [] + + async def list_user_tokens(username: str) -> dict: """列出某引擎用户的消费令牌(按用户名挂到引擎用户)。""" data = await _request( diff --git a/app/services/pineagents_catalog.py b/app/services/pineagents_catalog.py deleted file mode 100644 index 30b0fae..0000000 --- a/app/services/pineagents_catalog.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- 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] diff --git a/tests/test_compute_catalog.py b/tests/test_compute_catalog.py new file mode 100644 index 0000000..5f920d7 --- /dev/null +++ b/tests/test_compute_catalog.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +"""compute_catalog 模型映射(admin 模型 → 桌面端形状)纯函数测试。""" +from __future__ import annotations + +from app.services.compute_catalog import _map + + +def test_map_admin_model(): + row = { + "model_name": "deepseek-chat", "name": "DeepSeek Chat", "actual_model": "deepseek-chat", + "group": "default", "billing_unit": "/百万tokens", "input_price": 0.5, "output_price": 1.5, + "cache_hit_price": 0.1, "vision_support": False, "image_support": False, + "audio_support": True, "video_support": False, "tags": "对话,通用", "vendor_id": 0, "status": 1, + } + m = _map(row) + assert m["id"] == "deepseek-chat" + assert m["name"] == "DeepSeek Chat" + assert m["input_price"] == 0.5 and m["output_price"] == 1.5 + assert m["audio_support"] is True + assert m["tags"] == ["对话", "通用"] + + +def test_map_falls_back_to_model_name_when_empty_name(): + m = _map({"model_name": "gpt-4o", "name": ""}) + assert m["id"] == "gpt-4o" and m["name"] == "gpt-4o" and m["actual_model"] == "gpt-4o"