From eee2f6547b5bff5926baecdbb77c1e22d782a72a Mon Sep 17 00:00:00 2001 From: Pine Date: Tue, 25 Aug 2026 18:03:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(pineagents):=20=E6=A8=A1=E5=9E=8B=E9=9D=99?= =?UTF-8?q?=E6=80=81=E5=B1=95=E7=A4=BA=E7=9B=AE=E5=BD=95(=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0/=E4=BB=B7=E6=A0=BC/=E5=88=86=E7=B1=BB/=E6=A0=87?= =?UTF-8?q?=E7=AD=BE)=20+=20ModelInfo=20=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../providers/pineagents_catalog.py | 220 ++++++++++++++++++ src/pineagents/providers/provider.py | 16 ++ src/pineagents/providers/provider_manager.py | 4 +- tests/test_pineagents_catalog.py | 67 ++++++ 4 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 src/pineagents/providers/pineagents_catalog.py create mode 100644 tests/test_pineagents_catalog.py diff --git a/src/pineagents/providers/pineagents_catalog.py b/src/pineagents/providers/pineagents_catalog.py new file mode 100644 index 0000000..0284806 --- /dev/null +++ b/src/pineagents/providers/pineagents_catalog.py @@ -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 diff --git a/src/pineagents/providers/provider.py b/src/pineagents/providers/provider.py index 565fb46..1e4a4d0 100644 --- a/src/pineagents/providers/provider.py +++ b/src/pineagents/providers/provider.py @@ -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): diff --git a/src/pineagents/providers/provider_manager.py b/src/pineagents/providers/provider_manager.py index 2d1d141..cde7588 100644 --- a/src/pineagents/providers/provider_manager.py +++ b/src/pineagents/providers/provider_manager.py @@ -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 = [ diff --git a/tests/test_pineagents_catalog.py b/tests/test_pineagents_catalog.py new file mode 100644 index 0000000..77f7312 --- /dev/null +++ b/tests/test_pineagents_catalog.py @@ -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