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:
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user