feat(pineagents): 模型静态展示目录(描述/价格/分类/标签) + ModelInfo 扩展

- ModelInfo 追加可选 description/price/category/tags(默认为空,不破坏现有构造,经
  model_dump + response_model 自动透传前端,无需改序列化)。
- 新增 providers/pineagents_catalog.py:按模型 id 的静态展示目录(覆盖 Qwen/DeepSeek/
  GLM/Kimi/MiniMax/MiMo/ModelScope 等) + enrich_catalog() 在拉取结果上回填;未命中
  优雅降级为仅名+能力标签,不修改调用参数。
- provider_manager.list_provider_info:pineagents 拉取成功后回填目录元数据。
- 新增 tests/test_pineagents_catalog.py(命中/免费/未命中/空/大小写),6 passed。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-25 18:03:18 +08:00
parent 4f665a6034
commit eee2f6547b
4 changed files with 306 additions and 1 deletions
@@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
"""PineAgents(算力中心)模型静态展示目录。
PineAgents 的模型列表由 server-core `/v1/models`compute-engine new-api)动态拉取,
`fetch_models()` 只保留 `id/name`。这里补一份按模型 id 的**展示元数据**(描述/价格/分类/标签),
在拉取结果上回填,供桌面端「设置 → 模型 → PineAgents」的市场风卡片渲染。
约定:
- key 用小写模型 id,匹配时做大小写归一;未命中的 id 由调用方跳过,优雅降级为「仅名 + 能力标签」。
- 价格 `price` 为展示字符串,形如 `¥x / 1M tokens`;免费模型用 `is_free=True` 打免费徽标。
- 仅用于展示,不改模型调用行为;后端不参与计费(计费由 compute-engine 负责)。
"""
from __future__ import annotations
from typing import List
from .provider import ModelInfo
# 每个模型一条展示元数据
_META = {
# ---- Qwen 系列(文本旗舰) ----
"qwen3.7-max": {
"description": "通义千问系列最强旗舰模型,复杂推理与长程任务首选,思维链推理开箱即用。",
"price": "¥28 / 1M tokens",
"category": "文本",
"tags": ["旗舰", "推理"],
},
"qwen3.7-plus": {
"description": "通义千问高性能均衡款,图像/视频多模态输入,适合生产级智能体场景。",
"price": "¥9 / 1M tokens",
"category": "文本",
"tags": ["多模态", "均衡"],
},
"qwen3.6-plus": {
"description": "通义千问通用款,多模态能力与性价比平衡,日常对话与工具调用主力。",
"price": "¥6 / 1M tokens",
"category": "文本",
"tags": ["多模态"],
},
"qwen3.6-flash": {
"description": "通义千问轻量高速款,低延迟低成本,适合高并发实时场景。",
"price": "¥2 / 1M tokens",
"category": "文本",
"tags": ["轻量", "低延迟"],
},
"qwen3.5-plus": {
"description": "通义千问上一代通用款,成熟稳定,兼容多模态输入。",
"price": "¥4 / 1M tokens",
"category": "文本",
"tags": ["多模态"],
},
"qwen3-max-2026-01-23": {
"description": "通义千问旗舰系列(1 月快照),强推理与海量上下文,适合 Agent 长链调度。",
"price": "¥24 / 1M tokens",
"category": "文本",
"tags": ["旗舰", "长上下文"],
},
"qwen3-coder-next": {
"description": "通义千问代码专项旗舰,面向编程任务的推理与生成优化,支持长代码库理解。",
"price": "¥16 / 1M tokens",
"category": "文本",
"tags": ["代码", "旗舰"],
},
"qwen3-coder-plus": {
"description": "通义千问代码通用款,代码补全与理解的高性价比选择。",
"price": "¥6 / 1M tokens",
"category": "文本",
"tags": ["代码"],
},
# ---- 视觉/图像 ----
"qwen-image-3.0-pro": {
"description": "通义千问图像生成旗舰,高质量文生图与图像编辑,多轮文本指令可控。",
"price": "¥0.4 / 张",
"category": "图片",
"tags": ["文生图", "生成"],
},
# ---- 视频 ----
"wan3.0-video-prime": {
"description": "万相视频生成旗舰,文本/图像转视频,支持长镜头一致性与运动连贯。",
"price": "¥2 / 秒",
"category": "视频",
"tags": ["文生视频", "生成"],
},
# ---- DeepSeek ----
"deepseek-v4-pro": {
"description": "DeepSeek 深度思考旗舰,强推理与代码能力,支持 effort 思考强度调节。",
"price": "¥8 / 1M tokens",
"category": "文本",
"tags": ["推理", "代码"],
"is_free": False,
},
"deepseek-v4-flash": {
"description": "DeepSeek 轻量款,推理与回复速度相配,适合高吞吐场景。",
"price": "¥2 / 1M tokens",
"category": "文本",
"tags": ["轻量"],
},
"deepseek-v3.2": {
"description": "DeepSeek 通用款,成熟稳定的对话与能力调用体验。",
"price": "¥3 / 1M tokens",
"category": "文本",
"tags": ["均衡"],
},
# ---- GLM(智谱) ----
"glm-5.2": {
"description": "智谱 GLM 旗舰,通用能力与推理全价覆盖,支持 effort 思考强度。",
"price": "¥12 / 1M tokens",
"category": "文本",
"tags": ["旗舰", "推理"],
},
"glm-5.1": {
"description": "智谱 GLM 上一代旗舰,成熟稳定,通用场景可靠。",
"price": "¥8 / 1M tokens",
"category": "文本",
"tags": ["均衡"],
},
"glm-5": {
"description": "智谱 GLM 高性能款,覆盖编码与复杂任务。",
"price": "¥5 / 1M tokens",
"category": "文本",
"tags": ["推理"],
},
"glm-4.7": {
"description": "智谱 GLM 通用款,性价比与能力均衡。",
"price": "¥3 / 1M tokens",
"category": "文本",
"tags": ["均衡"],
},
"glm-4.7-flash": {
"description": "智谱 GLM 轻量高速款,低延迟低成本。",
"price": "¥1 / 1M tokens",
"category": "文本",
"tags": ["轻量", "低延迟"],
},
# ---- Kimi(月之暗面) ----
"kimi-k2.6": {
"description": "Kimi 最新旗舰,长上下文与强推理,适合复杂工具链调度。",
"price": "¥10 / 1M tokens",
"category": "文本",
"tags": ["旗舰", "长上下文"],
},
"kimi-k2.5": {
"description": "Kimi 长上下文通用款,读长文档与多项工具调用表现均衡。",
"price": "¥6 / 1M tokens",
"category": "文本",
"tags": ["长上下文"],
},
# ---- MiniMax ----
"MiniMax-M2.5": {
"description": "MiniMax 旗舰,语言理解与生成能力强,多语言友好。",
"price": "¥4 / 1M tokens",
"category": "文本",
"tags": ["均衡", "多语言"],
},
# ---- MiMo(小米) ----
"mimo-v2.5-pro": {
"description": "MiMo 推理旗舰,面向复杂推理与长任务。",
"price": "¥8 / 1M tokens",
"category": "文本",
"tags": ["推理"],
},
"mimo-v2.5": {
"description": "MiMo 多模态通用款,图像输入与对话体验均衡。",
"price": "¥4 / 1M tokens",
"category": "文本",
"tags": ["多模态"],
},
# ---- ModelScope(开源/免费) ----
"Qwen/Qwen3.5-122B-A10B": {
"description": "ModelScope 开源 Qwen3.5 混合专家模型,支持图像/视频输入,免费可用。",
"price": "免费",
"category": "文本",
"tags": ["开源", "多模态"],
"is_free": True,
},
"ZhipuAI/GLM-5": {
"description": "ModelScope 开源智谱 GLM-5,纯文本强推理,免费可用。",
"price": "免费",
"category": "文本",
"tags": ["开源", "推理"],
"is_free": True,
},
}
# 大小写归一化的查找表(引擎可能返回不同大小写的 id)
_LOOKUP = {key.strip().lower(): val for key, val in _META.items()}
# 公共别名(供测试/调用方读取目录规模)
PINEAGENTS_MODEL_CATALOG = _META
def enrich_catalog(models: List[ModelInfo]) -> List[ModelInfo]:
"""按模型 id 从静态目录回填展示元数据;未命中的模型原样返回。
命中时写入 `description/price/category/tags/is_free`;均覆盖为目录值,
但不改变模型的调用参数(max_tokens/thinking 等不受影响)。
"""
if not models:
return models
enriched: List[ModelInfo] = []
for model in models:
meta = _LOOKUP.get((model.id or "").strip().lower())
if meta is None:
enriched.append(model)
continue
enriched.append(
ModelInfo(
**model.model_dump(
exclude={"description", "price", "category", "tags", "is_free"},
),
description=meta.get("description"),
price=meta.get("price"),
category=meta.get("category"),
tags=meta.get("tags", []),
is_free=bool(meta.get("is_free", model.is_free)),
)
)
return enriched
+16
View File
@@ -118,6 +118,22 @@ class ModelInfo(BaseModel):
description="Override provider-level thinking_budget_range [min, max] "
"for this model.",
)
description: str | None = Field(
default=None,
description="Human-readable model description (marketplace/display).",
)
price: str | None = Field(
default=None,
description="Display price for the model, e.g. '¥2.5 / 1M tokens'.",
)
category: str | None = Field(
default=None,
description="Model category, e.g. 文本/图片/视频/音频.",
)
tags: List[str] = Field(
default_factory=list,
description="Display tags for the model (e.g. 旗舰/推理/视觉).",
)
class ExtendedModelInfo(ModelInfo):
+3 -1
View File
@@ -37,6 +37,7 @@ from .openai_provider import (
)
from .openai_response_provider import OpenAIResponseProvider
from .openrouter_provider import OpenRouterProvider
from .pineagents_catalog import enrich_catalog
from .provider import ModelInfo, Provider, ProviderInfo
logger = logging.getLogger(__name__)
@@ -1460,7 +1461,8 @@ class ProviderManager: # pylint: disable=too-many-public-methods
try:
fetched = await pine.fetch_models()
if fetched:
pine.models = fetched
# 回填静态展示元数据(描述/价格/分类/标签),未命中的模型保持原样。
pine.models = enrich_catalog(fetched)
except Exception: # noqa: BLE001
pass
tasks = [
+67
View File
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
"""PineAgents 模型静态展示目录单测。"""
import pytest
from pineagents.providers.pineagents_catalog import (
PINEAGENTS_MODEL_CATALOG as _catalog_pub,
enrich_catalog,
)
from pineagents.providers.provider import ModelInfo
pytestmark = pytest.mark.unit
def test_enrich_catalog_hit_fills_metadata():
"""命中目录:回填描述/价格/分类/标签/is_free,且不改调用参数。"""
src = [
ModelInfo(
id="qwen3.7-max",
name="Qwen3.7 Max",
supports_image=False,
supports_video=False,
thinking_enabled=True,
)
]
out = enrich_catalog(src)
m = out[0]
assert m.description
assert m.price
assert m.category == "文本"
assert m.tags
# 调用参数保持不变
assert m.thinking_enabled is True
assert m.supports_multimodal is None
def test_enrich_catalog_free_model():
"""免费模型 is_free 命中为 True。"""
out = enrich_catalog([ModelInfo(id="Qwen/Qwen3.5-122B-A10B", name="Q3.5")])
assert out[0].is_free is True
assert out[0].price == "免费"
def test_enrich_catalog_miss_degrades():
"""未命中目录:原样返回,不填充展示字段。"""
out = enrich_catalog([ModelInfo(id="unknown-model-xyz", name="X")])
m = out[0]
assert m.id == "unknown-model-xyz"
assert m.name == "X"
assert m.description is None
assert m.price is None
assert m.tags == []
def test_enrich_catalog_empty():
assert enrich_catalog([]) == []
def test_enrich_catalog_case_insensitive():
"""id 大小写不同仍能命中(查找表已归一化)。"""
out = enrich_catalog([ModelInfo(id="MINIMAX-M2.5", name="MM")])
assert out[0].category == "文本"
def test_catalog_public_entries():
"""公共目录表不应为空(供回填使用)。"""
assert len(_catalog_pub) > 0