feat(compute): 把 admin 模型开放给 OPC 与 /v1(算力中心化)

- 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 通过
This commit is contained in:
Pine
2026-08-25 13:47:08 +08:00
parent 6242717e7b
commit 865f937f41
6 changed files with 101 additions and 54 deletions
+5 -5
View File
@@ -14,7 +14,7 @@ from ..schemas.opc import BidRequest, ProfileUpdate, FinanceRecordCreate
from ...rbac import require_roles, write_audit from ...rbac import require_roles, write_audit
from ...infrastructure.repositories import Database, new_id, utcnow_iso from ...infrastructure.repositories import Database, new_id, utcnow_iso
from ...infrastructure.models import FinanceRecord 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"]) 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"))): 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"))): 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="我的算力令牌") @router.get("/compute/tokens", summary="我的算力令牌")
+4 -13
View File
@@ -17,17 +17,10 @@ from fastapi.responses import JSONResponse, StreamingResponse
from starlette.background import BackgroundTask from starlette.background import BackgroundTask
from ... import config from ... import config
from ...services import compute_catalog
router = APIRouter(prefix="/v1", tags=["relay"]) 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]: def _auth_headers() -> dict[str, str]:
# /v1 转发用引擎能识别的真实消费令牌(PINEAGENTS_COMPUTE_RELAY_TOKEN), # /v1 转发用引擎能识别的真实消费令牌(PINEAGENTS_COMPUTE_RELAY_TOKEN),
@@ -86,11 +79,9 @@ async def relay_chat_completions(request: Request):
@router.get("/models") @router.get("/models")
async def relay_models(): async def relay_models():
"""返回模型清单(供 OpenAIProvider.fetch_models 使用)。""" """返回模型清单(= 算力中心 admin 模型,供 OpenAIProvider.fetch_models 使用)。"""
ids = await compute_catalog.model_ids()
return { return {
"object": "list", "object": "list",
"data": [ "data": [{"id": m, "object": "model", "owned_by": "pineagents"} for m in ids],
{"id": m, "object": "model", "owned_by": "pineagents"}
for m in _ENGINE_MODEL_IDS
],
} }
+58
View File
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
"""算力中心模型目录:把 compute 引擎的 admin 模型(models 表,status=1)映射为桌面端可读形状。
替代原静态 pineagents_catalogOPC 桌面端展示/使用的模型 = 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"]]
+9
View File
@@ -119,6 +119,15 @@ async def issue_user_token(username: str, name: str = "") -> dict:
return data.get("data") or {} 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: async def list_user_tokens(username: str) -> dict:
"""列出某引擎用户的消费令牌(按用户名挂到引擎用户)。""" """列出某引擎用户的消费令牌(按用户名挂到引擎用户)。"""
data = await _request( data = await _request(
-36
View File
@@ -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]
+25
View File
@@ -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"