whosellm 0.3.1__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {whosellm-0.3.1 → whosellm-0.4.0}/.bumpversion.toml +1 -1
- {whosellm-0.3.1 → whosellm-0.4.0}/CHANGELOG.md +10 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/PKG-INFO +30 -1
- {whosellm-0.3.1 → whosellm-0.4.0}/README.md +29 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/docs/add_new_model_family.md +13 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/pyproject.toml +1 -1
- whosellm-0.4.0/tests/test_tool_result_surfaces.py +279 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/uv.lock +1 -1
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/__init__.py +14 -2
- whosellm-0.4.0/whosellm/capabilities.py +238 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/alibaba.py +31 -1
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/anthropic.py +93 -1
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/deepseek/deepseek_official.py +67 -1
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/gemini.py +138 -1
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_3_5.py +4 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_4.py +4 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_4_1.py +10 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_4o.py +14 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5.py +14 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_1.py +11 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_2.py +12 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_3.py +4 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_4.py +16 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_5.py +10 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_6.py +10 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_6.py +7 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_o1.py +10 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_o3.py +14 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/openai_o4.py +10 -0
- whosellm-0.4.0/whosellm/models/families/openai/surfaces.py +70 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/zhipu.py +49 -1
- whosellm-0.3.1/whosellm/capabilities.py +0 -114
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/commands/interview.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/projects/-Users-jqq-PycharmProjects-llmeta/memory/MEMORY.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/projects/-Users-jqq-PycharmProjects-llmeta/memory/feedback_use_uv.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/settings.json +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/code-review/SKILL.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/create-skill/SKILL.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/e2e-metadata/SKILL.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/evolve/SKILL.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/fix-review/SKILL.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/release/SKILL.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/review-provider-model/SKILL.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/alibaba.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/anthropic.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/deepseek.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/gemini.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/openai.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/others.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/vidu.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/zhipu.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/update-provider-model/SKILL.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.claude/skills/update-provider-model/testing.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.coveragerc +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.github/workflows/publish.yml +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.github/workflows/tests.yml +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.gitignore +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.windsurf/workflows/addmodel.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.windsurf/workflows/arch.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/.windsurf/workflows/testllmeta.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/CLAUDE.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/LICENSE +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/docs/refactor_proposal.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/docs/spec_model_family_redesign.md +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/examples/advanced_usage.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/examples/basic_usage.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/mypy.ini +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/pytest.ini +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/ruff.toml +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/__init__.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/e2e/__init__.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/e2e/conftest.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/e2e/test_anthropic.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/e2e/test_deepseek.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/e2e/test_google.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/e2e/test_openai.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/e2e/test_zhipu.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/__init__.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/__init__.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_anthropic.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_deepseek.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_deepseek_tencent.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_gemini.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_glm41v_thinking.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_glm45.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_glm45v.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_glm46.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_glm46v.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_glm5.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_glm53.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_glm5v_turbo.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_gpt3_5.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_gpt4_1.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_gpt4o.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_gpt5.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_gpt6.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_media_count_limit_config.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_o1.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_o3.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_o4.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_qwen.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_qwen3_vl_models.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_qwen_ollama.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/models/families/test_qwen_plus.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/test_auto_register.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/test_claude_4x_versions.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/test_llmeta.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/test_media_count_limit.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/test_model_version.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/test_provider.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/test_registry_merge.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/test_specific_patterns.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/tests/test_variant_priority_config.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/model_version.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/__init__.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/base.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/config.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/dynamic_enum.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/__init__.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/deepseek/__init__.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/deepseek/tencent.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/openai/__init__.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/others.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/families/vidu.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/patterns.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/models/registry.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/provider.py +0 -0
- {whosellm-0.3.1 → whosellm-0.4.0}/whosellm/py.typed +0 -0
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **工具结果多模态能力(TFROB-856)**:新增 `LLMApiSurface`(str 枚举:`claude_messages` / `openai_chat_completions` / `openai_responses` / `gemini` / `dashscope` / `glm` / `deepseek` / `deepseek_responses`)与 `ToolResultSurfaceCapability`(`modalities` / `max_inline_bytes` / `mime_types`)。`ModelCapabilities.tool_result_surfaces` 以 surface 为键登记工具结果的原生媒体能力:缺键 = 未声明,空元组 = 明确不支持。
|
|
9
|
+
- 新增文档上限字段:`max_pdf_size_mb`、`max_pdf_pages`、`max_document_size_mb`(预留)、`supported_document_mime_type`。`None` 表示未声明(未知),不代表“不限”;单文件与请求级上限分层登记。
|
|
10
|
+
- 新增单请求内联媒体总预算 `max_inline_media_bytes` 与文件类媒体合计上限 `max_inline_file_bytes`(媒体计数字段继续复用 `media_count_limit`)。
|
|
11
|
+
- 预留 `FileApiCapability` / `ModelCapabilities.file_api`,本版本不消费。
|
|
12
|
+
- 数据登记(官方文档核实 + 2026-10-29 抓取日期;未逐模型 API 实测的已显式标注):Claude 4.x/5 工具结果支持 image + PDF;OpenAI Chat Completions tool 消息 text-only、Responses `function_call_output` 支持 image/file;Gemini 3 系支持 image,Gemini 2.x 显式标为不支持;DeepSeek Chat Completions tool 消息 text-only,Responses `function_call_output` 支持 image,PDF `input_file` 经 2026-10-29 实测为静默不投递(登记为不支持)。DashScope / GLM 的官方 API 参考将 tool 消息 content 定义为字符串,登记为显式不支持(空元组)。
|
|
13
|
+
- 修正 OpenAI gpt-5.1 及更新视觉模型缺失的 `supports_pdf=True`(官方 PDF 解析要求视觉模型,gpt-4o 及更新)。
|
|
14
|
+
|
|
5
15
|
## [0.2.4] - Unreleased
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: whosellm
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A unified LLM model version and capability management library
|
|
5
5
|
Author-email: JQQ <jqq1716@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -146,6 +146,35 @@ print(model.capabilities.max_video_size_mb) # 20.0
|
|
|
146
146
|
validated_params = model.validate_params(your_params)
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
+
### 工具结果的多模态能力 / Multimodal Tool Results
|
|
150
|
+
|
|
151
|
+
工具结果能否原生承载图片/文档,取决于 **provider × API surface**,而不是模型名字前缀。能力数据以 `LLMApiSurface` 为键,缺键表示「未声明」,空元组表示「明确不支持」——两者都应按不支持处理。
|
|
152
|
+
|
|
153
|
+
Whether a tool result can natively carry images/documents depends on **provider × API surface**, not on a model-name prefix. Capability data is keyed by `LLMApiSurface`; a missing key means "undeclared" and an empty tuple means "explicitly unsupported" — treat both as unsupported.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from whosellm import LLMApiSurface, LLMeta
|
|
157
|
+
|
|
158
|
+
caps = LLMeta("claude-opus-5-5").capabilities
|
|
159
|
+
surface = caps.tool_result_surfaces.get(LLMApiSurface.CLAUDE_MESSAGES)
|
|
160
|
+
print(surface.modalities) # ("image", "document")
|
|
161
|
+
print(surface.supports("document")) # True
|
|
162
|
+
|
|
163
|
+
# Gemini 3 系支持;2.5 系明确不支持(会返回 400)
|
|
164
|
+
print(LLMeta("gemini-3.8-flash").capabilities.tool_result_surfaces[LLMApiSurface.GEMINI].modalities) # ("image",)
|
|
165
|
+
print(LLMeta("gemini-2.5-pro").capabilities.tool_result_surfaces[LLMApiSurface.GEMINI].modalities) # ()
|
|
166
|
+
|
|
167
|
+
# DeepSeek 的 Chat Completions 与 Responses 是两个 surface:
|
|
168
|
+
# Chat Completions tool 消息只能放文本;Responses 的 function_call_output 支持图片,
|
|
169
|
+
# 但 PDF input_file 实测被静默丢弃(2026-10-29 探针)
|
|
170
|
+
print(LLMeta("deepseek-flash").capabilities.tool_result_surfaces[LLMApiSurface.DEEPSEEK].modalities) # ()
|
|
171
|
+
print(LLMeta("deepseek-flash").capabilities.tool_result_surfaces[LLMApiSurface.DEEPSEEK_RESPONSES].modalities) # ("image",)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
用户消息能力仍由 `supports_vision` / `supports_audio` / `supports_video` / `supports_pdf` 表示,工具结果能力只走 `tool_result_surfaces`。文档上限见 `max_pdf_size_mb`、`max_pdf_pages`、`supported_document_mime_type`;单请求内联媒体预算见 `max_inline_media_bytes`,文件类媒体的请求级合计上限见 `max_inline_file_bytes`。
|
|
175
|
+
|
|
176
|
+
User-message capability remains on `supports_vision` / `supports_audio` / `supports_video` / `supports_pdf`; tool-result capability lives only in `tool_result_surfaces`. Document limits are exposed via `max_pdf_size_mb`, `max_pdf_pages`, and `supported_document_mime_type`; the per-request inline-media budget is `max_inline_media_bytes`, and the request-level combined cap for file-type media is `max_inline_file_bytes`.
|
|
177
|
+
|
|
149
178
|
### 非法名称也不会炸 / Lenient on Unknown Names
|
|
150
179
|
|
|
151
180
|
我们不想打断你的流程,即便名称不在我们的数据库里也能返回一个「未知模型占位符」。
|
|
@@ -121,6 +121,35 @@ print(model.capabilities.max_video_size_mb) # 20.0
|
|
|
121
121
|
validated_params = model.validate_params(your_params)
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
### 工具结果的多模态能力 / Multimodal Tool Results
|
|
125
|
+
|
|
126
|
+
工具结果能否原生承载图片/文档,取决于 **provider × API surface**,而不是模型名字前缀。能力数据以 `LLMApiSurface` 为键,缺键表示「未声明」,空元组表示「明确不支持」——两者都应按不支持处理。
|
|
127
|
+
|
|
128
|
+
Whether a tool result can natively carry images/documents depends on **provider × API surface**, not on a model-name prefix. Capability data is keyed by `LLMApiSurface`; a missing key means "undeclared" and an empty tuple means "explicitly unsupported" — treat both as unsupported.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from whosellm import LLMApiSurface, LLMeta
|
|
132
|
+
|
|
133
|
+
caps = LLMeta("claude-opus-5-5").capabilities
|
|
134
|
+
surface = caps.tool_result_surfaces.get(LLMApiSurface.CLAUDE_MESSAGES)
|
|
135
|
+
print(surface.modalities) # ("image", "document")
|
|
136
|
+
print(surface.supports("document")) # True
|
|
137
|
+
|
|
138
|
+
# Gemini 3 系支持;2.5 系明确不支持(会返回 400)
|
|
139
|
+
print(LLMeta("gemini-3.8-flash").capabilities.tool_result_surfaces[LLMApiSurface.GEMINI].modalities) # ("image",)
|
|
140
|
+
print(LLMeta("gemini-2.5-pro").capabilities.tool_result_surfaces[LLMApiSurface.GEMINI].modalities) # ()
|
|
141
|
+
|
|
142
|
+
# DeepSeek 的 Chat Completions 与 Responses 是两个 surface:
|
|
143
|
+
# Chat Completions tool 消息只能放文本;Responses 的 function_call_output 支持图片,
|
|
144
|
+
# 但 PDF input_file 实测被静默丢弃(2026-10-29 探针)
|
|
145
|
+
print(LLMeta("deepseek-flash").capabilities.tool_result_surfaces[LLMApiSurface.DEEPSEEK].modalities) # ()
|
|
146
|
+
print(LLMeta("deepseek-flash").capabilities.tool_result_surfaces[LLMApiSurface.DEEPSEEK_RESPONSES].modalities) # ("image",)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
用户消息能力仍由 `supports_vision` / `supports_audio` / `supports_video` / `supports_pdf` 表示,工具结果能力只走 `tool_result_surfaces`。文档上限见 `max_pdf_size_mb`、`max_pdf_pages`、`supported_document_mime_type`;单请求内联媒体预算见 `max_inline_media_bytes`,文件类媒体的请求级合计上限见 `max_inline_file_bytes`。
|
|
150
|
+
|
|
151
|
+
User-message capability remains on `supports_vision` / `supports_audio` / `supports_video` / `supports_pdf`; tool-result capability lives only in `tool_result_surfaces`. Document limits are exposed via `max_pdf_size_mb`, `max_pdf_pages`, and `supported_document_mime_type`; the per-request inline-media budget is `max_inline_media_bytes`, and the request-level combined cap for file-type media is `max_inline_file_bytes`.
|
|
152
|
+
|
|
124
153
|
### 非法名称也不会炸 / Lenient on Unknown Names
|
|
125
154
|
|
|
126
155
|
我们不想打断你的流程,即便名称不在我们的数据库里也能返回一个「未知模型占位符」。
|
|
@@ -197,6 +197,19 @@ __all__ = [
|
|
|
197
197
|
| `max_image_pixels` | `tuple[int, int] \| None` | 最大图片像素(宽, 高) / Max image pixels (width, height) |
|
|
198
198
|
| `max_video_size_mb` | `float \| None` | 最大视频大小(MB) / Max video size in MB |
|
|
199
199
|
| `max_video_duration_seconds` | `int \| None` | 最大视频时长(秒) / Max video duration in seconds |
|
|
200
|
+
| `media_count_limit` | `MediaCountLimit` | 单请求媒体数量上限 / Per-request media count caps |
|
|
201
|
+
| `max_inline_media_bytes` | `int \| None` | 单请求内联媒体字节总预算 / Per-request inline-media byte budget |
|
|
202
|
+
| `max_inline_file_bytes` | `int \| None` | 单请求文件类媒体字节合计上限 / Combined per-request file-type media byte cap |
|
|
203
|
+
| `max_pdf_size_mb` | `float \| None` | PDF 大小上限(MB) / Max PDF size in MB |
|
|
204
|
+
| `max_pdf_pages` | `int \| None` | PDF 页数上限 / Max PDF pages per request |
|
|
205
|
+
| `max_document_size_mb` | `float \| None` | 非 PDF 文档大小上限(MB,预留) / Max non-PDF document size |
|
|
206
|
+
| `supported_document_mime_type` | `list[str]` | 支持的文档 MIME / Supported document MIME types |
|
|
207
|
+
| `tool_result_surfaces` | `dict[LLMApiSurface, ToolResultSurfaceCapability]` | 工具结果按 API surface 的原生媒体能力;缺键 = 未声明,空元组 = 明确不支持 / Native tool-result media capability per surface; missing = undeclared, empty tuple = unsupported |
|
|
208
|
+
| `file_api` | `FileApiCapability \| None` | 文件 API 能力(预留)/ File API capability (reserved) |
|
|
209
|
+
|
|
210
|
+
工具结果能力是 **provider × API surface** 维度,而不是模型维度。新增模型时应按官方文档登记 `tool_result_surfaces`,并注明来源与抓取日期;未查证的 surface 请保持缺键(内核按不支持处理),不要乐观填写。
|
|
211
|
+
|
|
212
|
+
Tool-result capability is **provider × API surface**, not per-model. When adding models, register `tool_result_surfaces` from official docs with source and fetch date; leave unverified surfaces undeclared (consumers treat them as unsupported) instead of guessing.
|
|
200
213
|
|
|
201
214
|
---
|
|
202
215
|
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# filename: test_tool_result_surfaces.py
|
|
2
|
+
"""
|
|
3
|
+
工具结果能力与文档限制测试 / Tool-result capability and document-limit tests
|
|
4
|
+
|
|
5
|
+
覆盖 W2–W5 新增字段:
|
|
6
|
+
- LLMApiSurface 词表与 ToolResultSurfaceCapability
|
|
7
|
+
- ModelCapabilities.tool_result_surfaces / max_inline_media_bytes
|
|
8
|
+
- max_pdf_size_mb / max_pdf_pages / supported_document_mime_type
|
|
9
|
+
- FileApiCapability(预留)
|
|
10
|
+
|
|
11
|
+
数据来源与抓取日期见 whosellm/models/families 下各配置文件的注释。
|
|
12
|
+
Data sources and fetch dates are documented in the family config modules.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from dataclasses import FrozenInstanceError
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from whosellm import (
|
|
21
|
+
FileApiCapability,
|
|
22
|
+
LLMApiSurface,
|
|
23
|
+
LLMeta,
|
|
24
|
+
ModelCapabilities,
|
|
25
|
+
ToolResultSurfaceCapability,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
REQUIRED_SURFACES = {
|
|
29
|
+
"claude_messages",
|
|
30
|
+
"openai_chat_completions",
|
|
31
|
+
"openai_responses",
|
|
32
|
+
"gemini",
|
|
33
|
+
"dashscope",
|
|
34
|
+
"glm",
|
|
35
|
+
"deepseek",
|
|
36
|
+
"deepseek_responses",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestLLMApiSurface:
|
|
41
|
+
"""API surface 词表 / API surface vocabulary"""
|
|
42
|
+
|
|
43
|
+
def test_required_members_exist(self) -> None:
|
|
44
|
+
assert {surface.value for surface in LLMApiSurface} == REQUIRED_SURFACES
|
|
45
|
+
|
|
46
|
+
def test_str_enum_serializes_as_plain_string(self) -> None:
|
|
47
|
+
"""str 枚举可直接作为 dict 键并 JSON 序列化 / str enum works as a dict key and serializes"""
|
|
48
|
+
assert LLMApiSurface.CLAUDE_MESSAGES == "claude_messages"
|
|
49
|
+
assert json.dumps({LLMApiSurface.CLAUDE_MESSAGES: "ok"}) == '{"claude_messages": "ok"}'
|
|
50
|
+
|
|
51
|
+
def test_value_lookup(self) -> None:
|
|
52
|
+
"""可用字符串值还原枚举成员 / Members can be reconstructed from their string values"""
|
|
53
|
+
assert LLMApiSurface("claude_messages") is LLMApiSurface.CLAUDE_MESSAGES
|
|
54
|
+
assert str(LLMApiSurface.GEMINI) == "gemini"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestDefaults:
|
|
58
|
+
"""新增字段的空默认值 / Empty defaults for the new fields"""
|
|
59
|
+
|
|
60
|
+
def test_tool_result_surface_defaults(self) -> None:
|
|
61
|
+
surface = ToolResultSurfaceCapability()
|
|
62
|
+
assert surface.modalities == ()
|
|
63
|
+
assert surface.max_inline_bytes is None
|
|
64
|
+
assert surface.mime_types is None
|
|
65
|
+
assert surface.supports("image") is False
|
|
66
|
+
|
|
67
|
+
def test_model_capabilities_defaults(self) -> None:
|
|
68
|
+
caps = ModelCapabilities()
|
|
69
|
+
assert caps.tool_result_surfaces == {}
|
|
70
|
+
assert caps.max_inline_media_bytes is None
|
|
71
|
+
assert caps.max_inline_file_bytes is None
|
|
72
|
+
assert caps.max_pdf_size_mb is None
|
|
73
|
+
assert caps.max_pdf_pages is None
|
|
74
|
+
assert caps.max_document_size_mb is None
|
|
75
|
+
assert caps.supported_document_mime_type == []
|
|
76
|
+
assert caps.file_api is None
|
|
77
|
+
|
|
78
|
+
def test_file_api_defaults(self) -> None:
|
|
79
|
+
file_api = FileApiCapability()
|
|
80
|
+
assert file_api.supports_upload is False
|
|
81
|
+
assert file_api.max_file_size_mb is None
|
|
82
|
+
assert file_api.supported_mime_types == []
|
|
83
|
+
|
|
84
|
+
def test_surface_is_frozen(self) -> None:
|
|
85
|
+
surface = ToolResultSurfaceCapability(modalities=("image",))
|
|
86
|
+
with pytest.raises(FrozenInstanceError):
|
|
87
|
+
surface.modalities = ("audio",) # type: ignore[misc]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestClaudeToolResults:
|
|
91
|
+
"""Claude Messages:工具结果原生支持 image + document / Claude Messages tool results"""
|
|
92
|
+
|
|
93
|
+
@pytest.mark.parametrize(
|
|
94
|
+
("model_name", "expected_pages"),
|
|
95
|
+
[
|
|
96
|
+
("claude-opus-5-5", 600),
|
|
97
|
+
("claude-fable-5", 600),
|
|
98
|
+
("claude-sonnet-5", 600),
|
|
99
|
+
("claude-opus-4-8", 600),
|
|
100
|
+
("claude-sonnet-4-6", 600),
|
|
101
|
+
("claude-sonnet-4-5", 100),
|
|
102
|
+
("claude-haiku-4-5", 100),
|
|
103
|
+
("claude-opus-4-1", 100),
|
|
104
|
+
],
|
|
105
|
+
)
|
|
106
|
+
def test_claude_4x_5x(self, model_name: str, expected_pages: int) -> None:
|
|
107
|
+
caps = LLMeta(model_name).capabilities
|
|
108
|
+
surface = caps.tool_result_surfaces[LLMApiSurface.CLAUDE_MESSAGES]
|
|
109
|
+
|
|
110
|
+
assert surface.modalities == ("image", "document")
|
|
111
|
+
assert surface.supports("image") is True
|
|
112
|
+
assert surface.supports("document") is True
|
|
113
|
+
assert surface.supports("audio") is False
|
|
114
|
+
assert surface.supports("video") is False
|
|
115
|
+
assert surface.mime_types is not None
|
|
116
|
+
assert surface.mime_types["document"] == ["application/pdf"]
|
|
117
|
+
assert surface.mime_types["image"] == ["image/jpeg", "image/png", "image/gif", "image/webp"]
|
|
118
|
+
|
|
119
|
+
# W3 文档上限 / W3 document limits
|
|
120
|
+
# 官方只公布请求级 128 MB,没有单文件上限 → max_pdf_size_mb 保持 None
|
|
121
|
+
# Only a request-level 128 MB cap is documented, so the per-file size stays None
|
|
122
|
+
assert caps.supports_pdf is True
|
|
123
|
+
assert caps.max_pdf_size_mb is None
|
|
124
|
+
assert caps.max_pdf_pages == expected_pages
|
|
125
|
+
assert caps.supported_document_mime_type == ["application/pdf"]
|
|
126
|
+
assert caps.max_inline_media_bytes == 128_000_000
|
|
127
|
+
assert caps.max_inline_file_bytes is None # 官方无文件合计上限 / no combined file cap
|
|
128
|
+
|
|
129
|
+
def test_claude_3x_is_undeclared(self) -> None:
|
|
130
|
+
"""Claude 3.x 未登记工具结果 surface 数据 / Claude 3.x leaves surfaces undeclared"""
|
|
131
|
+
caps = LLMeta("claude-3-7-sonnet").capabilities
|
|
132
|
+
assert LLMApiSurface.CLAUDE_MESSAGES not in caps.tool_result_surfaces
|
|
133
|
+
assert caps.max_pdf_size_mb is None
|
|
134
|
+
assert caps.max_pdf_pages is None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TestOpenAIToolResults:
|
|
138
|
+
"""OpenAI:Chat Completions text-only;Responses 支持 image / document"""
|
|
139
|
+
|
|
140
|
+
@pytest.mark.parametrize("model_name", ["gpt-4o", "gpt-5.4-mini", "gpt-6-astra", "o3"])
|
|
141
|
+
def test_chat_completions_unsupported_responses_supported(self, model_name: str) -> None:
|
|
142
|
+
caps = LLMeta(model_name).capabilities
|
|
143
|
+
chat = caps.tool_result_surfaces[LLMApiSurface.OPENAI_CHAT_COMPLETIONS]
|
|
144
|
+
responses = caps.tool_result_surfaces[LLMApiSurface.OPENAI_RESPONSES]
|
|
145
|
+
|
|
146
|
+
assert chat.modalities == ()
|
|
147
|
+
assert chat.supports("image") is False
|
|
148
|
+
assert responses.modalities == ("image", "document")
|
|
149
|
+
assert responses.supports("image") is True
|
|
150
|
+
assert responses.supports("document") is True
|
|
151
|
+
assert responses.mime_types is not None
|
|
152
|
+
assert responses.mime_types["document"] == ["application/pdf"]
|
|
153
|
+
|
|
154
|
+
assert caps.supports_pdf is True
|
|
155
|
+
assert caps.max_pdf_size_mb == 50.0
|
|
156
|
+
assert caps.max_pdf_pages is None # 官方未公布页数上限 / no documented page cap
|
|
157
|
+
assert caps.supported_document_mime_type == ["application/pdf"]
|
|
158
|
+
assert caps.max_inline_media_bytes == 512_000_000
|
|
159
|
+
assert caps.max_inline_file_bytes == 50_000_000 # 所有文件合计 <50 MB / combined files
|
|
160
|
+
|
|
161
|
+
@pytest.mark.parametrize("model_name", ["o3-mini", "gpt-4o-audio-preview"])
|
|
162
|
+
def test_non_vision_chat_only(self, model_name: str) -> None:
|
|
163
|
+
caps = LLMeta(model_name).capabilities
|
|
164
|
+
assert caps.tool_result_surfaces[LLMApiSurface.OPENAI_CHAT_COMPLETIONS].modalities == ()
|
|
165
|
+
assert LLMApiSurface.OPENAI_RESPONSES not in caps.tool_result_surfaces
|
|
166
|
+
|
|
167
|
+
def test_gpt_3_5_chat_only(self) -> None:
|
|
168
|
+
caps = LLMeta("gpt-3.5-turbo").capabilities
|
|
169
|
+
assert caps.tool_result_surfaces[LLMApiSurface.OPENAI_CHAT_COMPLETIONS].modalities == ()
|
|
170
|
+
assert caps.max_pdf_size_mb is None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class TestGeminiToolResults:
|
|
174
|
+
"""Gemini:仅 3 系支持多模态 function response;2.x 必须显式标不支持"""
|
|
175
|
+
|
|
176
|
+
@pytest.mark.parametrize(
|
|
177
|
+
"model_name",
|
|
178
|
+
["gemini-3.8-flash", "gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-3-pro"],
|
|
179
|
+
)
|
|
180
|
+
def test_gemini_3_supports_image(self, model_name: str) -> None:
|
|
181
|
+
caps = LLMeta(model_name).capabilities
|
|
182
|
+
surface = caps.tool_result_surfaces[LLMApiSurface.GEMINI]
|
|
183
|
+
assert surface.modalities == ("image",)
|
|
184
|
+
assert surface.supports("image") is True
|
|
185
|
+
assert surface.supports("document") is False # 官方 FunctionResult 仅登记 image / only image is documented
|
|
186
|
+
|
|
187
|
+
# 官方未公布单文件 PDF 上限(inline payload 100 MB / PDF 50 MB 均为请求级)
|
|
188
|
+
# No per-file PDF cap is documented; the inline payload caps are request-level
|
|
189
|
+
assert caps.max_pdf_size_mb is None
|
|
190
|
+
assert caps.max_pdf_pages == 1000
|
|
191
|
+
assert caps.supported_document_mime_type == ["application/pdf"]
|
|
192
|
+
assert caps.max_inline_media_bytes == 100_000_000
|
|
193
|
+
assert caps.max_inline_file_bytes == 50_000_000
|
|
194
|
+
|
|
195
|
+
@pytest.mark.parametrize(
|
|
196
|
+
"model_name",
|
|
197
|
+
["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.0-flash"],
|
|
198
|
+
)
|
|
199
|
+
def test_gemini_2x_explicitly_unsupported(self, model_name: str) -> None:
|
|
200
|
+
"""2.x 明确标为不支持(空元组),而非缺键 / 2.x is explicitly unsupported, not undeclared"""
|
|
201
|
+
caps = LLMeta(model_name).capabilities
|
|
202
|
+
surface = caps.tool_result_surfaces[LLMApiSurface.GEMINI]
|
|
203
|
+
assert surface.modalities == ()
|
|
204
|
+
assert surface.supports("image") is False
|
|
205
|
+
|
|
206
|
+
@pytest.mark.parametrize("model_name", ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite"])
|
|
207
|
+
def test_gemini_2_5_pdf_limits(self, model_name: str) -> None:
|
|
208
|
+
"""用户消息的 PDF 能力不受工具结果限制影响 / User-message PDF capability is unaffected"""
|
|
209
|
+
caps = LLMeta(model_name).capabilities
|
|
210
|
+
assert caps.supports_pdf is True
|
|
211
|
+
assert caps.max_pdf_pages == 1000
|
|
212
|
+
assert caps.max_pdf_size_mb is None
|
|
213
|
+
assert caps.max_inline_file_bytes == 50_000_000
|
|
214
|
+
|
|
215
|
+
def test_image_only_model_has_no_surface(self) -> None:
|
|
216
|
+
"""无 function calling 的模型不登记 surface / Models without function calling declare no surface"""
|
|
217
|
+
caps = LLMeta("gemini-2.5-flash-image").capabilities
|
|
218
|
+
assert LLMApiSurface.GEMINI not in caps.tool_result_surfaces
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class TestDeepSeekToolResults:
|
|
222
|
+
"""DeepSeek:Chat Completions text-only;Responses 图片可用、PDF 静默丢失"""
|
|
223
|
+
|
|
224
|
+
@pytest.mark.parametrize(
|
|
225
|
+
"model_name",
|
|
226
|
+
["deepseek-flash", "deepseek-v4-pro", "deepseek-chat", "deepseek-reasoner"],
|
|
227
|
+
)
|
|
228
|
+
def test_chat_completions_text_only(self, model_name: str) -> None:
|
|
229
|
+
caps = LLMeta(model_name).capabilities
|
|
230
|
+
surface = caps.tool_result_surfaces[LLMApiSurface.DEEPSEEK]
|
|
231
|
+
assert surface.modalities == ()
|
|
232
|
+
assert surface.supports("image") is False
|
|
233
|
+
|
|
234
|
+
# 实测 2026-10-29:PDF input_file 被接受但内容不投递 / PDF is silently dropped
|
|
235
|
+
assert caps.supports_pdf is False
|
|
236
|
+
assert caps.supported_document_mime_type == []
|
|
237
|
+
assert caps.max_pdf_size_mb is None
|
|
238
|
+
assert caps.max_pdf_pages is None
|
|
239
|
+
assert caps.max_inline_file_bytes is None
|
|
240
|
+
|
|
241
|
+
@pytest.mark.parametrize(
|
|
242
|
+
"model_name",
|
|
243
|
+
["deepseek-flash", "deepseek-chat", "deepseek-reasoner"],
|
|
244
|
+
)
|
|
245
|
+
def test_responses_supports_image_not_document(self, model_name: str) -> None:
|
|
246
|
+
caps = LLMeta(model_name).capabilities
|
|
247
|
+
surface = caps.tool_result_surfaces[LLMApiSurface.DEEPSEEK_RESPONSES]
|
|
248
|
+
assert surface.modalities == ("image",)
|
|
249
|
+
assert surface.supports("image") is True
|
|
250
|
+
assert surface.supports("document") is False
|
|
251
|
+
assert surface.max_inline_bytes == 32 * 1024 * 1024
|
|
252
|
+
assert surface.mime_types is not None
|
|
253
|
+
assert surface.mime_types["image"] == ["image/jpeg", "image/png", "image/gif", "image/webp"]
|
|
254
|
+
assert caps.max_inline_media_bytes == 48 * 1024 * 1024
|
|
255
|
+
|
|
256
|
+
def test_responses_text_only_model(self) -> None:
|
|
257
|
+
caps = LLMeta("deepseek-v4-pro").capabilities
|
|
258
|
+
surface = caps.tool_result_surfaces[LLMApiSurface.DEEPSEEK_RESPONSES]
|
|
259
|
+
assert surface.modalities == ()
|
|
260
|
+
assert caps.max_inline_media_bytes is None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class TestExplicitlyUnsupportedSurfaces:
|
|
264
|
+
"""官方明确不支持:显式空元组 / Officially unsupported: explicit empty tuple"""
|
|
265
|
+
|
|
266
|
+
@pytest.mark.parametrize(
|
|
267
|
+
("model_name", "surface"),
|
|
268
|
+
[
|
|
269
|
+
("qwen3-vl-plus", LLMApiSurface.DASHSCOPE),
|
|
270
|
+
("qwen-plus", LLMApiSurface.DASHSCOPE),
|
|
271
|
+
("glm-4.6v", LLMApiSurface.GLM),
|
|
272
|
+
("glm-5v-turbo", LLMApiSurface.GLM),
|
|
273
|
+
],
|
|
274
|
+
)
|
|
275
|
+
def test_dashscope_and_glm_text_only(self, model_name: str, surface: LLMApiSurface) -> None:
|
|
276
|
+
"""官方 API 参考将 tool 消息 content 定义为字符串 / Official API defines tool content as string"""
|
|
277
|
+
caps = LLMeta(model_name).capabilities
|
|
278
|
+
assert caps.tool_result_surfaces[surface].modalities == ()
|
|
279
|
+
assert caps.tool_result_surfaces[surface].supports("image") is False
|
|
@@ -7,19 +7,31 @@
|
|
|
7
7
|
LLMeta - 统一的大语言模型版本和能力管理库 / A unified LLM model version and capability management library
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "0.
|
|
10
|
+
__version__ = "0.4.0"
|
|
11
11
|
|
|
12
|
-
from whosellm.capabilities import
|
|
12
|
+
from whosellm.capabilities import (
|
|
13
|
+
UNLIMITED,
|
|
14
|
+
FileApiCapability,
|
|
15
|
+
LLMApiSurface,
|
|
16
|
+
MediaCountLimit,
|
|
17
|
+
ModelCapabilities,
|
|
18
|
+
ToolResultModality,
|
|
19
|
+
ToolResultSurfaceCapability,
|
|
20
|
+
)
|
|
13
21
|
from whosellm.model_version import LLMeta
|
|
14
22
|
from whosellm.models.base import ModelFamily
|
|
15
23
|
from whosellm.provider import Provider
|
|
16
24
|
|
|
17
25
|
__all__ = [
|
|
18
26
|
"UNLIMITED",
|
|
27
|
+
"FileApiCapability",
|
|
28
|
+
"LLMApiSurface",
|
|
19
29
|
"LLMeta",
|
|
20
30
|
"MediaCountLimit",
|
|
21
31
|
"ModelCapabilities",
|
|
22
32
|
"ModelFamily",
|
|
23
33
|
"Provider",
|
|
34
|
+
"ToolResultModality",
|
|
35
|
+
"ToolResultSurfaceCapability",
|
|
24
36
|
"__version__",
|
|
25
37
|
]
|