whosellm 0.3.0__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.
Files changed (131) hide show
  1. {whosellm-0.3.0 → whosellm-0.4.0}/.bumpversion.toml +1 -1
  2. {whosellm-0.3.0 → whosellm-0.4.0}/CHANGELOG.md +10 -0
  3. {whosellm-0.3.0 → whosellm-0.4.0}/PKG-INFO +30 -1
  4. {whosellm-0.3.0 → whosellm-0.4.0}/README.md +29 -0
  5. {whosellm-0.3.0 → whosellm-0.4.0}/docs/add_new_model_family.md +13 -0
  6. {whosellm-0.3.0 → whosellm-0.4.0}/pyproject.toml +1 -1
  7. {whosellm-0.3.0 → whosellm-0.4.0}/tests/e2e/test_anthropic.py +35 -1
  8. whosellm-0.4.0/tests/e2e/test_deepseek.py +151 -0
  9. {whosellm-0.3.0 → whosellm-0.4.0}/tests/e2e/test_openai.py +41 -1
  10. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_anthropic.py +68 -2
  11. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_deepseek.py +99 -0
  12. whosellm-0.4.0/tests/models/families/test_gpt6.py +166 -0
  13. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_media_count_limit_config.py +12 -2
  14. whosellm-0.4.0/tests/test_tool_result_surfaces.py +279 -0
  15. {whosellm-0.3.0 → whosellm-0.4.0}/tests/test_variant_priority_config.py +9 -6
  16. {whosellm-0.3.0 → whosellm-0.4.0}/uv.lock +1 -1
  17. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/__init__.py +14 -2
  18. whosellm-0.4.0/whosellm/capabilities.py +238 -0
  19. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/alibaba.py +31 -1
  20. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/anthropic.py +123 -1
  21. whosellm-0.4.0/whosellm/models/families/deepseek/deepseek_official.py +257 -0
  22. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/gemini.py +138 -1
  23. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_3_5.py +4 -0
  24. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_4.py +4 -0
  25. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_4_1.py +10 -0
  26. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_4o.py +14 -0
  27. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5.py +14 -0
  28. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_1.py +11 -0
  29. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_2.py +12 -0
  30. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_3.py +4 -0
  31. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_4.py +16 -0
  32. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_5.py +10 -0
  33. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_gpt_5_6.py +10 -0
  34. whosellm-0.4.0/whosellm/models/families/openai/openai_gpt_6.py +101 -0
  35. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_o1.py +10 -0
  36. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_o3.py +14 -0
  37. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/openai_o4.py +10 -0
  38. whosellm-0.4.0/whosellm/models/families/openai/surfaces.py +70 -0
  39. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/zhipu.py +49 -1
  40. whosellm-0.3.0/tests/models/families/test_gpt6.py +0 -70
  41. whosellm-0.3.0/whosellm/capabilities.py +0 -114
  42. whosellm-0.3.0/whosellm/models/families/deepseek/deepseek_official.py +0 -145
  43. whosellm-0.3.0/whosellm/models/families/openai/openai_gpt_6.py +0 -56
  44. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/commands/interview.md +0 -0
  45. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/projects/-Users-jqq-PycharmProjects-llmeta/memory/MEMORY.md +0 -0
  46. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/projects/-Users-jqq-PycharmProjects-llmeta/memory/feedback_use_uv.md +0 -0
  47. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/settings.json +0 -0
  48. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/code-review/SKILL.md +0 -0
  49. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/create-skill/SKILL.md +0 -0
  50. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/e2e-metadata/SKILL.md +0 -0
  51. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/evolve/SKILL.md +0 -0
  52. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/fix-review/SKILL.md +0 -0
  53. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/release/SKILL.md +0 -0
  54. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/review-provider-model/SKILL.md +0 -0
  55. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/alibaba.md +0 -0
  56. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/anthropic.md +0 -0
  57. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/deepseek.md +0 -0
  58. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/gemini.md +0 -0
  59. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/openai.md +0 -0
  60. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/others.md +0 -0
  61. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/vidu.md +0 -0
  62. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/review-provider-model/providers/zhipu.md +0 -0
  63. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/update-provider-model/SKILL.md +0 -0
  64. {whosellm-0.3.0 → whosellm-0.4.0}/.claude/skills/update-provider-model/testing.md +0 -0
  65. {whosellm-0.3.0 → whosellm-0.4.0}/.coveragerc +0 -0
  66. {whosellm-0.3.0 → whosellm-0.4.0}/.github/workflows/publish.yml +0 -0
  67. {whosellm-0.3.0 → whosellm-0.4.0}/.github/workflows/tests.yml +0 -0
  68. {whosellm-0.3.0 → whosellm-0.4.0}/.gitignore +0 -0
  69. {whosellm-0.3.0 → whosellm-0.4.0}/.windsurf/workflows/addmodel.md +0 -0
  70. {whosellm-0.3.0 → whosellm-0.4.0}/.windsurf/workflows/arch.md +0 -0
  71. {whosellm-0.3.0 → whosellm-0.4.0}/.windsurf/workflows/testllmeta.md +0 -0
  72. {whosellm-0.3.0 → whosellm-0.4.0}/CLAUDE.md +0 -0
  73. {whosellm-0.3.0 → whosellm-0.4.0}/LICENSE +0 -0
  74. {whosellm-0.3.0 → whosellm-0.4.0}/docs/refactor_proposal.md +0 -0
  75. {whosellm-0.3.0 → whosellm-0.4.0}/docs/spec_model_family_redesign.md +0 -0
  76. {whosellm-0.3.0 → whosellm-0.4.0}/examples/advanced_usage.py +0 -0
  77. {whosellm-0.3.0 → whosellm-0.4.0}/examples/basic_usage.py +0 -0
  78. {whosellm-0.3.0 → whosellm-0.4.0}/mypy.ini +0 -0
  79. {whosellm-0.3.0 → whosellm-0.4.0}/pytest.ini +0 -0
  80. {whosellm-0.3.0 → whosellm-0.4.0}/ruff.toml +0 -0
  81. {whosellm-0.3.0 → whosellm-0.4.0}/tests/__init__.py +0 -0
  82. {whosellm-0.3.0 → whosellm-0.4.0}/tests/e2e/__init__.py +0 -0
  83. {whosellm-0.3.0 → whosellm-0.4.0}/tests/e2e/conftest.py +0 -0
  84. {whosellm-0.3.0 → whosellm-0.4.0}/tests/e2e/test_google.py +0 -0
  85. {whosellm-0.3.0 → whosellm-0.4.0}/tests/e2e/test_zhipu.py +0 -0
  86. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/__init__.py +0 -0
  87. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/__init__.py +0 -0
  88. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_deepseek_tencent.py +0 -0
  89. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_gemini.py +0 -0
  90. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_glm41v_thinking.py +0 -0
  91. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_glm45.py +0 -0
  92. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_glm45v.py +0 -0
  93. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_glm46.py +0 -0
  94. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_glm46v.py +0 -0
  95. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_glm5.py +0 -0
  96. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_glm53.py +0 -0
  97. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_glm5v_turbo.py +0 -0
  98. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_gpt3_5.py +0 -0
  99. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_gpt4_1.py +0 -0
  100. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_gpt4o.py +0 -0
  101. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_gpt5.py +0 -0
  102. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_o1.py +0 -0
  103. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_o3.py +0 -0
  104. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_o4.py +0 -0
  105. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_qwen.py +0 -0
  106. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_qwen3_vl_models.py +0 -0
  107. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_qwen_ollama.py +0 -0
  108. {whosellm-0.3.0 → whosellm-0.4.0}/tests/models/families/test_qwen_plus.py +0 -0
  109. {whosellm-0.3.0 → whosellm-0.4.0}/tests/test_auto_register.py +0 -0
  110. {whosellm-0.3.0 → whosellm-0.4.0}/tests/test_claude_4x_versions.py +0 -0
  111. {whosellm-0.3.0 → whosellm-0.4.0}/tests/test_llmeta.py +0 -0
  112. {whosellm-0.3.0 → whosellm-0.4.0}/tests/test_media_count_limit.py +0 -0
  113. {whosellm-0.3.0 → whosellm-0.4.0}/tests/test_model_version.py +0 -0
  114. {whosellm-0.3.0 → whosellm-0.4.0}/tests/test_provider.py +0 -0
  115. {whosellm-0.3.0 → whosellm-0.4.0}/tests/test_registry_merge.py +0 -0
  116. {whosellm-0.3.0 → whosellm-0.4.0}/tests/test_specific_patterns.py +0 -0
  117. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/model_version.py +0 -0
  118. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/__init__.py +0 -0
  119. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/base.py +0 -0
  120. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/config.py +0 -0
  121. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/dynamic_enum.py +0 -0
  122. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/__init__.py +0 -0
  123. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/deepseek/__init__.py +0 -0
  124. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/deepseek/tencent.py +0 -0
  125. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/openai/__init__.py +0 -0
  126. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/others.py +0 -0
  127. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/families/vidu.py +0 -0
  128. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/patterns.py +0 -0
  129. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/models/registry.py +0 -0
  130. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/provider.py +0 -0
  131. {whosellm-0.3.0 → whosellm-0.4.0}/whosellm/py.typed +0 -0
@@ -2,7 +2,7 @@
2
2
  # 文档 / Documentation: https://callowayproject.github.io/bump-my-version/
3
3
 
4
4
  [tool.bumpversion]
5
- current_version = "0.3.0"
5
+ current_version = "0.4.0"
6
6
  parse = """(?x)
7
7
  (?P<major>0|[1-9]\\d*)\\.
8
8
  (?P<minor>0|[1-9]\\d*)\\.
@@ -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.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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "whosellm"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "A unified LLM model version and capability management library"
5
5
  authors = [
6
6
  {name = "JQQ", email = "jqq1716@gmail.com"}
@@ -2,6 +2,7 @@
2
2
 
3
3
  来源: https://platform.claude.com/docs/en/docs/about-claude/models
4
4
  采集日期: 2026-04-12(2026-09-04 增补 Fable 5.1 / Mythos 5.1)
5
+ 2026-09-23 增补 Opus 5.5
5
6
  """
6
7
 
7
8
  import pytest
@@ -10,6 +11,33 @@ from whosellm import ModelFamily, Provider
10
11
 
11
12
  from .conftest import assert_model_metadata
12
13
 
14
+ # ============================================================================
15
+ # Claude Family — Version 5.5 (2026-09-22 发布 / Released 2026-09-22)
16
+ # 来源: https://platform.claude.com/docs/en/release-notes/overview
17
+ # ============================================================================
18
+
19
+ CLAUDE_55_MODELS = [
20
+ (
21
+ "claude-opus-5-5",
22
+ {
23
+ # 1M ctx (默认且最大) / 128K out;adaptive thinking 常开且不可关闭
24
+ # 1M ctx (default & max) / 128K out; adaptive thinking always on, cannot be disabled
25
+ "provider": Provider.ANTHROPIC,
26
+ "family": ModelFamily.CLAUDE,
27
+ "version": "5.5",
28
+ "variant": "opus",
29
+ "supports_thinking": True,
30
+ "supports_vision": True,
31
+ "supports_streaming": True,
32
+ "supports_function_calling": True,
33
+ "supports_structured_outputs": True,
34
+ "supports_computer_use": True,
35
+ "context_window": 1_000_000,
36
+ "max_tokens": 128_000,
37
+ },
38
+ ),
39
+ ]
40
+
13
41
  # ============================================================================
14
42
  # Claude Family — Version 5.1 (2026-09-01 发布 / Released 2026-09-01)
15
43
  # 来源: https://platform.claude.com/docs/en/models/fable-5-1/overview
@@ -234,7 +262,13 @@ CLAUDE_30_MODELS = [
234
262
  # ============================================================================
235
263
 
236
264
  ALL_MODELS = (
237
- CLAUDE_51_MODELS + CLAUDE_LATEST_MODELS + CLAUDE_45_MODELS + CLAUDE_41_MODELS + CLAUDE_40_MODELS + CLAUDE_30_MODELS
265
+ CLAUDE_55_MODELS
266
+ + CLAUDE_51_MODELS
267
+ + CLAUDE_LATEST_MODELS
268
+ + CLAUDE_45_MODELS
269
+ + CLAUDE_41_MODELS
270
+ + CLAUDE_40_MODELS
271
+ + CLAUDE_30_MODELS
238
272
  )
239
273
 
240
274
 
@@ -0,0 +1,151 @@
1
+ # filename: test_deepseek.py
2
+ """
3
+ DeepSeek 官方模型元数据 E2E 测试 / DeepSeek official model metadata E2E tests.
4
+
5
+ 来源 / Sources:
6
+ https://api-docs.deepseek.com/zh-cn/updates/(2026-09-10 DeepSeek-V4.1-Flash 发布)
7
+ https://api-docs.deepseek.com/zh-cn/quick_start/pricing
8
+ https://api-docs.deepseek.com/zh-cn/guides/vision
9
+ 采集日期 / Collected: 2026-09-28(含 api.deepseek.com 实探复核 / verified against the live API)
10
+
11
+ 约定 / Conventions:
12
+ * 旧名(deepseek-v4-flash / -vision-exp / deepseek-chat / deepseek-reasoner)版本仍按名字解析为
13
+ 4.0,能力按当前实际服务模型(转发到 DeepSeek-V4.1-Flash)标注。
14
+ Legacy names keep their name-derived version (4.0) while capabilities follow the
15
+ currently serving model (routed to DeepSeek-V4.1-Flash).
16
+ * deepseek-v4-pro 未被路由,保持纯文本(无图像输入)。
17
+ """
18
+
19
+ import pytest
20
+
21
+ from whosellm import ModelFamily, Provider
22
+
23
+ from .conftest import assert_model_metadata
24
+
25
+ # ============================================================================
26
+ # DeepSeek V4.1-Flash 正式名(2026-09-10 发布)/ Canonical name of DeepSeek-V4.1-Flash
27
+ # 官方:"将模型名称更改为 deepseek-flash 即可调用最新的 V4.1 Flash 模型"
28
+ # ============================================================================
29
+
30
+ V41_FLASH_MODELS = [
31
+ (
32
+ "deepseek-flash",
33
+ {
34
+ "provider": Provider.DEEPSEEK,
35
+ "family": ModelFamily.DEEPSEEK,
36
+ # 名字无版本号 → 按官方模型版本 DeepSeek-V4.1-Flash 标注
37
+ "version": "4.1",
38
+ "variant": "flash",
39
+ "supports_thinking": True, # 默认开启,effort 默认 high
40
+ "supports_vision": True, # 原生多模态视觉理解
41
+ "supports_function_calling": True,
42
+ "supports_streaming": True,
43
+ "supports_json_outputs": True,
44
+ "supports_structured_outputs": False, # 仅 json_object,无 json_schema
45
+ "context_window": 1_000_000,
46
+ "max_tokens": 384_000,
47
+ },
48
+ ),
49
+ ]
50
+
51
+ # ============================================================================
52
+ # 兼容路由旧名(官方措辞"暂时路由")/ Routed legacy names ("temporarily routed" per vendor)
53
+ # ============================================================================
54
+
55
+ ROUTED_LEGACY_MODELS = [
56
+ (
57
+ "deepseek-v4-flash",
58
+ {
59
+ "provider": Provider.DEEPSEEK,
60
+ "family": ModelFamily.DEEPSEEK,
61
+ "version": "4.0", # 版本轴按名字 / version parses from the name
62
+ "variant": "flash",
63
+ "supports_thinking": True,
64
+ "supports_vision": True, # 能力轴跟实际服务模型 / capabilities follow the serving model
65
+ "supports_function_calling": True,
66
+ "supports_streaming": True,
67
+ "context_window": 1_000_000,
68
+ "max_tokens": 384_000,
69
+ },
70
+ ),
71
+ (
72
+ "deepseek-v4-flash-vision-exp",
73
+ {
74
+ "provider": Provider.DEEPSEEK,
75
+ "family": ModelFamily.DEEPSEEK,
76
+ "version": "4.0",
77
+ "variant": "flash-vision-exp",
78
+ "supports_thinking": True,
79
+ "supports_vision": True,
80
+ "context_window": 1_000_000,
81
+ "max_tokens": 384_000,
82
+ },
83
+ ),
84
+ (
85
+ "deepseek-chat",
86
+ {
87
+ "provider": Provider.DEEPSEEK,
88
+ "family": ModelFamily.DEEPSEEK,
89
+ "version": "4.0",
90
+ "variant": "chat",
91
+ "supports_thinking": False, # 别名默认非思考模式 / alias defaults to non-thinking
92
+ "supports_vision": True,
93
+ "context_window": 1_000_000,
94
+ "max_tokens": 384_000,
95
+ },
96
+ ),
97
+ (
98
+ "deepseek-reasoner",
99
+ {
100
+ "provider": Provider.DEEPSEEK,
101
+ "family": ModelFamily.DEEPSEEK,
102
+ "version": "4.0",
103
+ "variant": "reasoner",
104
+ "supports_thinking": True, # 别名默认思考模式 / alias defaults to thinking
105
+ "supports_vision": True,
106
+ "context_window": 1_000_000,
107
+ "max_tokens": 384_000,
108
+ },
109
+ ),
110
+ ]
111
+
112
+ # ============================================================================
113
+ # 未被路由:deepseek-v4-pro 仍是 DeepSeek-V4-Pro-0813,纯文本
114
+ # Not routed: deepseek-v4-pro is still DeepSeek-V4-Pro-0813, text-only
115
+ # 实探 2026-09-28:带图不报错但静默忽略并编造答案 → supports_vision 必须为 False
116
+ # ============================================================================
117
+
118
+ TEXT_ONLY_MODELS = [
119
+ (
120
+ "deepseek-v4-pro",
121
+ {
122
+ "provider": Provider.DEEPSEEK,
123
+ "family": ModelFamily.DEEPSEEK,
124
+ "version": "4.0",
125
+ "variant": "pro",
126
+ "supports_thinking": True,
127
+ "supports_vision": False,
128
+ "supports_function_calling": True,
129
+ "supports_streaming": True,
130
+ "context_window": 1_000_000,
131
+ "max_tokens": 384_000,
132
+ },
133
+ ),
134
+ ]
135
+
136
+ # ============================================================================
137
+ # 聚合 + 参数化 / Aggregation & parametrization
138
+ # ============================================================================
139
+
140
+ ALL_MODELS = V41_FLASH_MODELS + ROUTED_LEGACY_MODELS + TEXT_ONLY_MODELS
141
+
142
+
143
+ @pytest.mark.e2e
144
+ @pytest.mark.parametrize(
145
+ "model_name,expected",
146
+ ALL_MODELS,
147
+ ids=[m[0] for m in ALL_MODELS],
148
+ )
149
+ def test_model_metadata(model_name: str, expected: dict) -> None: # type: ignore[type-arg]
150
+ """验证 DeepSeek 模型元数据与官方文档一致。"""
151
+ assert_model_metadata(model_name, expected)
@@ -15,6 +15,7 @@ from .conftest import assert_model_metadata
15
15
  # 来源: https://developers.openai.com/api/docs/models/gpt-5.4(2026-04-12 采集)
16
16
  # https://developers.openai.com/api/docs/models/gpt-5.6-sol(2026-08-20 采集 5.5/5.6)
17
17
  # https://developers.openai.com/api/docs/models/gpt-6-astra(2026-09-04 采集 6-astra/5.6-cyber)
18
+ # https://developers.openai.com/api/docs/models/gpt-6-sol(2026-09-23 采集 6-sol/6-luna)
18
19
  # ============================================================================
19
20
 
20
21
  GPT_MODELS = [
@@ -219,7 +220,7 @@ GPT_MODELS = [
219
220
  },
220
221
  ),
221
222
  # ------------------------------------------------------------------
222
- # GPT-6 Astra 系列(2026-09-04 采集)
223
+ # GPT-6 天体档位系列(2026-09-04 采集 astra)
223
224
  # 来源: https://developers.openai.com/api/docs/models/gpt-6-astra
224
225
  # ------------------------------------------------------------------
225
226
  (
@@ -239,6 +240,45 @@ GPT_MODELS = [
239
240
  "max_tokens": 128_000,
240
241
  },
241
242
  ),
243
+ # ------------------------------------------------------------------
244
+ # GPT-6 Sol / Luna(2026-09-22 发布,2026-09-23 采集)
245
+ # 与 astra 同窗口同工具面;sol 中档主力、luna 低成本档,GPT-6 无 terra 档
246
+ # 来源: https://developers.openai.com/api/docs/models/gpt-6-sol
247
+ # ------------------------------------------------------------------
248
+ (
249
+ "gpt-6-sol",
250
+ {
251
+ "provider": Provider.OPENAI,
252
+ "family": ModelFamily.GPT,
253
+ "version": "6.0",
254
+ "variant": "sol",
255
+ "supports_thinking": True,
256
+ "supports_vision": True,
257
+ "supports_streaming": True,
258
+ "supports_function_calling": True,
259
+ "supports_structured_outputs": True,
260
+ "supports_computer_use": True,
261
+ "context_window": 1_050_000,
262
+ "max_tokens": 128_000,
263
+ },
264
+ ),
265
+ (
266
+ "gpt-6-luna",
267
+ {
268
+ "provider": Provider.OPENAI,
269
+ "family": ModelFamily.GPT,
270
+ "version": "6.0",
271
+ "variant": "luna",
272
+ "supports_thinking": True,
273
+ "supports_vision": True,
274
+ "supports_streaming": True,
275
+ "supports_function_calling": True,
276
+ "supports_structured_outputs": True,
277
+ "supports_computer_use": True,
278
+ "context_window": 1_050_000,
279
+ "max_tokens": 128_000,
280
+ },
281
+ ),
242
282
  ]
243
283
 
244
284
  # ============================================================================
@@ -253,7 +253,7 @@ class TestClaudeMythosClassOrdering:
253
253
  assert LLMeta("claude-mythos-5") > LLMeta("claude-fable-5")
254
254
 
255
255
  def test_mythos5_is_top(self):
256
- """mythos-5 为当前最高 / mythos-5 is the highest among current Claude models"""
256
+ """mythos-5 在所列模型中最高 / mythos-5 is the highest among the models listed"""
257
257
  models = [
258
258
  LLMeta("claude-mythos-5"),
259
259
  LLMeta("claude-fable-5"),
@@ -264,6 +264,57 @@ class TestClaudeMythosClassOrdering:
264
264
  assert max(models).variant == "mythos"
265
265
 
266
266
 
267
+ class TestClaudeOpus55:
268
+ """Claude Opus 5.5 测试(2026-09-22 发布,当前最新) / Claude Opus 5.5 tests (released 2026-09-22)"""
269
+
270
+ def test_specific_model_config(self):
271
+ """验证 claude-opus-5-5 配置 / Validate claude-opus-5-5 config"""
272
+ config = get_specific_model_config("claude-opus-5-5")
273
+ assert config is not None
274
+ version, variant, capabilities = config
275
+ assert version == "5.5"
276
+ assert variant == "opus"
277
+ assert capabilities is not None
278
+ assert capabilities.supports_vision is True
279
+ assert capabilities.supports_thinking is True # 自适应思考常开且不可关闭 / always on, cannot be disabled
280
+ assert capabilities.supports_function_calling is True
281
+ assert capabilities.supports_streaming is True
282
+ assert capabilities.supports_structured_outputs is True
283
+ assert capabilities.supports_computer_use is True
284
+ assert capabilities.max_tokens == 128000
285
+ assert capabilities.context_window == 1000000
286
+
287
+ def test_pattern_match(self):
288
+ """验证 claude-opus-5-5 模式匹配 / Validate claude-opus-5-5 pattern match"""
289
+ matched = match_model_pattern("claude-opus-5-5")
290
+ assert matched is not None
291
+ assert matched["family"] == ModelFamily.CLAUDE
292
+ assert matched["variant"] == "opus"
293
+ assert matched["provider"] == Provider.ANTHROPIC
294
+ assert matched["_from_specific_model"] == "claude-opus-5-5"
295
+
296
+ @pytest.mark.parametrize("model_name", ["claude-opus-5-5-20260921", "claude-opus-5-5@20260921"])
297
+ def test_pattern_with_snapshot(self, model_name: str):
298
+ """验证带 snapshot 的解析(- 与 @ 两种格式,版本号不被吞) / Validate snapshot forms keep version 5.5"""
299
+ meta = LLMeta(model_name)
300
+ assert meta.family == ModelFamily.CLAUDE
301
+ assert meta.version == "5.5"
302
+ assert meta.variant == "opus"
303
+
304
+ def test_not_swallowed_by_opus5(self):
305
+ """claude-opus-5-5 不得被 claude-opus-5 的配置吞掉 / opus-5-5 must not be captured by opus-5
306
+
307
+ 回归保护:opus-5 的子模式在 opus-5-5 之前注册,若 snapshot 宽度匹配不当会退化为 5.0
308
+ Regression guard: opus-5's sub-patterns register before opus-5-5's; a loose snapshot
309
+ width would silently degrade opus-5-5 to version 5.0
310
+ """
311
+ meta = LLMeta("claude-opus-5-5")
312
+ assert meta.version == "5.5"
313
+ assert meta.variant == "opus"
314
+ assert meta.capabilities.context_window == 1000000
315
+ assert meta.capabilities.max_tokens == 128000
316
+
317
+
267
318
  class TestClaudeOpus5:
268
319
  """Claude Opus 5 测试(2026-07-24 GA) / Claude Opus 5 tests"""
269
320
 
@@ -345,6 +396,19 @@ class TestClaude5Ordering:
345
396
  """opus-5 (v5.0) 高于 opus-4-8 (v4.8) / opus-5 outranks opus-4-8 by version"""
346
397
  assert LLMeta("claude-opus-5") > LLMeta("claude-opus-4-8")
347
398
 
399
+ def test_opus55_outranks_opus5(self):
400
+ """opus-5-5 (v5.5) 高于 opus-5 (v5.0) / opus-5-5 outranks opus-5 by version"""
401
+ assert LLMeta("claude-opus-5-5") > LLMeta("claude-opus-5")
402
+ assert LLMeta("claude-opus-5-5") > LLMeta("claude-fable-5-1")
403
+
404
+ def test_opus55_outranks_mythos5_by_version(self):
405
+ """版本优先于档位:opus-5-5 (v5.5) 高于 mythos-5 (v5.0),尽管 mythos 变体档更高
406
+ Version precedes tier: opus-5-5 outranks mythos-5 despite mythos's higher variant tier
407
+ """
408
+ assert LLMeta("claude-opus-5-5") > LLMeta("claude-mythos-5")
409
+ # 同版本 5.0 内档位排序不受影响 / tier ordering within v5.0 is unaffected
410
+ assert LLMeta("claude-mythos-5") > LLMeta("claude-fable-5")
411
+
348
412
  def test_sonnet5_outranks_sonnet46(self):
349
413
  """sonnet-5 (v5.0) 高于 sonnet-4-6 (v4.6) / sonnet-5 outranks sonnet-4-6 by version"""
350
414
  assert LLMeta("claude-sonnet-5") > LLMeta("claude-sonnet-4-6")
@@ -358,7 +422,9 @@ class TestClaude5Ordering:
358
422
  assert LLMeta("claude-fable-5") > LLMeta("claude-opus-5")
359
423
 
360
424
  def test_mythos5_is_top(self):
361
- """mythos-5 仍为当前最高 / mythos-5 remains the highest among current Claude models"""
425
+ """同代 v5.0 内 mythos 档最高(v5.1/v5.5 按版本另行比较)
426
+ mythos is the top tier within the v5.0 generation (v5.1/v5.5 compare by version)
427
+ """
362
428
  models = [
363
429
  LLMeta("claude-mythos-5"),
364
430
  LLMeta("claude-fable-5"),
@@ -209,3 +209,102 @@ def test_deepseek_v4_flash_vision_exp_llmeta() -> None:
209
209
  assert model.variant == "flash-vision-exp"
210
210
  assert model.capabilities.context_window == 1_000_000
211
211
  assert model.capabilities.supports_vision is True
212
+
213
+
214
+ class TestDeepSeekRouting:
215
+ """DeepSeek 旧模型名的路由实况 / Routing reality of DeepSeek legacy names.
216
+
217
+ 官方原文(2026-09-10 changelog):"旧版本模型 V4 Flash 与 V4 Flash Vision Exp 现已下线,
218
+ 出于兼容考虑,模型名 deepseek-v4-flash、deepseek-v4-flash-vision-exp 将被暂时路由到 V4.1 Flash。"
219
+ Official: legacy V4 Flash / V4 Flash Vision Exp are offline; their model names are
220
+ *temporarily* routed to V4.1 Flash.
221
+
222
+ 实探证据(2026-09-28,api.deepseek.com,见 /tmp/probe_deepseek.py):
223
+ - GET /models 仅返回 deepseek-flash 与 deepseek-v4-pro
224
+ - deepseek-v4-flash / deepseek-v4-flash-vision-exp / deepseek-chat / deepseek-reasoner
225
+ 的响应 model 字段均回显 deepseek-flash,且能正确读出白底红方块
226
+ - deepseek-v4-pro 回显自身,带图不报错但静默忽略并编造答案
227
+
228
+ ⚠️ 路由是"暂时"的:官方撤销后本类测试会失败,届时需重新探针并回改配置
229
+ (能力轴跟端点现状,版本轴跟名字血统)。/ The routing is temporary: when the vendor
230
+ removes it these tests fail on purpose, prompting a re-probe and a config rollback.
231
+ """
232
+
233
+ def test_deepseek_flash_canonical_name(self) -> None:
234
+ """deepseek-flash 为官方正式名(无版本号),对应 DeepSeek-V4.1-Flash"""
235
+ from whosellm import LLMeta
236
+
237
+ model = LLMeta("deepseek-flash")
238
+ assert model.provider == Provider.DEEPSEEK
239
+ assert model.family == ModelFamily.DEEPSEEK
240
+ # 名字无版本号 → 按官方模型版本标注 4.1
241
+ assert model.version == "4.1"
242
+ assert model.variant == "flash"
243
+
244
+ caps = model.capabilities
245
+ assert caps.supports_vision is True
246
+ assert caps.supports_thinking is True # 默认开启 / on by default
247
+ assert caps.media_count_limit.get("image") == 600
248
+ assert caps.context_window == 1_000_000
249
+ assert caps.max_tokens == 384_000
250
+
251
+ def test_v4_flash_routed_to_v41_flash(self) -> None:
252
+ """deepseek-v4-flash 已被路由到 V4.1 Flash,视觉能力随之可用"""
253
+ from whosellm import LLMeta
254
+
255
+ model = LLMeta("deepseek-v4-flash")
256
+ # 版本轴仍按名字解析为 4.0 / version still parses from the name (4.0)
257
+ assert model.version == "4.0"
258
+ assert model.variant == "flash"
259
+ # 能力轴跟随实际服务模型 / capabilities follow the serving model
260
+ assert model.capabilities.supports_vision is True
261
+ assert model.capabilities.media_count_limit.get("image") == 600
262
+
263
+ def test_vision_exp_routed_to_v41_flash(self) -> None:
264
+ """deepseek-v4-flash-vision-exp 同样被路由到 V4.1 Flash"""
265
+ from whosellm import LLMeta
266
+
267
+ model = LLMeta("deepseek-v4-flash-vision-exp")
268
+ assert model.version == "4.0"
269
+ assert model.capabilities.supports_vision is True
270
+ assert model.capabilities.media_count_limit.get("image") == 600
271
+
272
+ def test_chat_and_reasoner_routed_have_vision(self) -> None:
273
+ """deepseek-chat / deepseek-reasoner 路由到 flash,同样可读图
274
+
275
+ 两者保留各自的默认思考模式语义(chat 非思考、reasoner 思考),
276
+ 但视觉能力跟随实际服务模型。
277
+ """
278
+ from whosellm import LLMeta
279
+
280
+ chat = LLMeta("deepseek-chat")
281
+ assert chat.capabilities.supports_thinking is False # 默认非思考 / non-thinking by default
282
+ assert chat.capabilities.supports_vision is True
283
+ assert chat.capabilities.media_count_limit.get("image") == 600
284
+
285
+ reasoner = LLMeta("deepseek-reasoner")
286
+ assert reasoner.capabilities.supports_thinking is True
287
+ assert reasoner.capabilities.supports_vision is True
288
+ assert reasoner.capabilities.media_count_limit.get("image") == 600
289
+
290
+ def test_v4_pro_not_routed_stays_text_only(self) -> None:
291
+ """deepseek-v4-pro 未被路由,保持纯文本(带图会静默幻觉,绝不能标 True)
292
+
293
+ 实探:带图请求返回 200,图片被静默忽略,答案系编造;无图对照亦编造;
294
+ 直接追问时模型承认"看不到图片 / [Unsupported Image]"。
295
+ 来源:https://api-docs.deepseek.com/zh-cn/quick_start/pricing(图像理解 不支持)
296
+ """
297
+ from whosellm import LLMeta
298
+
299
+ model = LLMeta("deepseek-v4-pro")
300
+ assert model.version == "4.0"
301
+ assert model.variant == "pro"
302
+ assert model.capabilities.supports_vision is False
303
+ assert model.capabilities.media_count_limit.get("image") == 0 # 键缺失视同不支持
304
+
305
+ def test_version_axis_orders_flash_above_pro(self) -> None:
306
+ """版本优先于档位:V4.1 Flash > V4 Pro(与官方"全面超越 V4 Pro"表述一致)"""
307
+ from whosellm import LLMeta
308
+
309
+ assert LLMeta("deepseek-flash") > LLMeta("deepseek-v4-pro")
310
+ assert LLMeta("deepseek-v4-pro") > LLMeta("deepseek-v4-flash")