whosellm 0.2.7__tar.gz → 0.3.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 (124) hide show
  1. {whosellm-0.2.7 → whosellm-0.3.0}/.bumpversion.toml +1 -1
  2. {whosellm-0.2.7 → whosellm-0.3.0}/CLAUDE.md +1 -0
  3. {whosellm-0.2.7 → whosellm-0.3.0}/PKG-INFO +1 -1
  4. {whosellm-0.2.7 → whosellm-0.3.0}/pyproject.toml +1 -1
  5. {whosellm-0.2.7 → whosellm-0.3.0}/tests/e2e/test_anthropic.py +3 -1
  6. whosellm-0.3.0/tests/models/families/test_media_count_limit_config.py +74 -0
  7. whosellm-0.3.0/tests/test_media_count_limit.py +114 -0
  8. {whosellm-0.2.7 → whosellm-0.3.0}/uv.lock +1 -1
  9. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/__init__.py +4 -2
  10. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/capabilities.py +48 -0
  11. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/alibaba.py +7 -1
  12. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/anthropic.py +34 -1
  13. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/deepseek/deepseek_official.py +4 -1
  14. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/gemini.py +7 -1
  15. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_5_5.py +4 -1
  16. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_5_6.py +4 -1
  17. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/zhipu.py +10 -1
  18. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/commands/interview.md +0 -0
  19. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/projects/-Users-jqq-PycharmProjects-llmeta/memory/MEMORY.md +0 -0
  20. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/projects/-Users-jqq-PycharmProjects-llmeta/memory/feedback_use_uv.md +0 -0
  21. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/settings.json +0 -0
  22. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/code-review/SKILL.md +0 -0
  23. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/create-skill/SKILL.md +0 -0
  24. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/e2e-metadata/SKILL.md +0 -0
  25. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/evolve/SKILL.md +0 -0
  26. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/fix-review/SKILL.md +0 -0
  27. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/release/SKILL.md +0 -0
  28. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/review-provider-model/SKILL.md +0 -0
  29. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/review-provider-model/providers/alibaba.md +0 -0
  30. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/review-provider-model/providers/anthropic.md +0 -0
  31. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/review-provider-model/providers/deepseek.md +0 -0
  32. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/review-provider-model/providers/gemini.md +0 -0
  33. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/review-provider-model/providers/openai.md +0 -0
  34. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/review-provider-model/providers/others.md +0 -0
  35. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/review-provider-model/providers/vidu.md +0 -0
  36. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/review-provider-model/providers/zhipu.md +0 -0
  37. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/update-provider-model/SKILL.md +0 -0
  38. {whosellm-0.2.7 → whosellm-0.3.0}/.claude/skills/update-provider-model/testing.md +0 -0
  39. {whosellm-0.2.7 → whosellm-0.3.0}/.coveragerc +0 -0
  40. {whosellm-0.2.7 → whosellm-0.3.0}/.github/workflows/publish.yml +0 -0
  41. {whosellm-0.2.7 → whosellm-0.3.0}/.github/workflows/tests.yml +0 -0
  42. {whosellm-0.2.7 → whosellm-0.3.0}/.gitignore +0 -0
  43. {whosellm-0.2.7 → whosellm-0.3.0}/.windsurf/workflows/addmodel.md +0 -0
  44. {whosellm-0.2.7 → whosellm-0.3.0}/.windsurf/workflows/arch.md +0 -0
  45. {whosellm-0.2.7 → whosellm-0.3.0}/.windsurf/workflows/testllmeta.md +0 -0
  46. {whosellm-0.2.7 → whosellm-0.3.0}/CHANGELOG.md +0 -0
  47. {whosellm-0.2.7 → whosellm-0.3.0}/LICENSE +0 -0
  48. {whosellm-0.2.7 → whosellm-0.3.0}/README.md +0 -0
  49. {whosellm-0.2.7 → whosellm-0.3.0}/docs/add_new_model_family.md +0 -0
  50. {whosellm-0.2.7 → whosellm-0.3.0}/docs/refactor_proposal.md +0 -0
  51. {whosellm-0.2.7 → whosellm-0.3.0}/docs/spec_model_family_redesign.md +0 -0
  52. {whosellm-0.2.7 → whosellm-0.3.0}/examples/advanced_usage.py +0 -0
  53. {whosellm-0.2.7 → whosellm-0.3.0}/examples/basic_usage.py +0 -0
  54. {whosellm-0.2.7 → whosellm-0.3.0}/mypy.ini +0 -0
  55. {whosellm-0.2.7 → whosellm-0.3.0}/pytest.ini +0 -0
  56. {whosellm-0.2.7 → whosellm-0.3.0}/ruff.toml +0 -0
  57. {whosellm-0.2.7 → whosellm-0.3.0}/tests/__init__.py +0 -0
  58. {whosellm-0.2.7 → whosellm-0.3.0}/tests/e2e/__init__.py +0 -0
  59. {whosellm-0.2.7 → whosellm-0.3.0}/tests/e2e/conftest.py +0 -0
  60. {whosellm-0.2.7 → whosellm-0.3.0}/tests/e2e/test_google.py +0 -0
  61. {whosellm-0.2.7 → whosellm-0.3.0}/tests/e2e/test_openai.py +0 -0
  62. {whosellm-0.2.7 → whosellm-0.3.0}/tests/e2e/test_zhipu.py +0 -0
  63. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/__init__.py +0 -0
  64. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/__init__.py +0 -0
  65. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_anthropic.py +0 -0
  66. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_deepseek.py +0 -0
  67. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_deepseek_tencent.py +0 -0
  68. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_gemini.py +0 -0
  69. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_glm41v_thinking.py +0 -0
  70. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_glm45.py +0 -0
  71. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_glm45v.py +0 -0
  72. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_glm46.py +0 -0
  73. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_glm46v.py +0 -0
  74. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_glm5.py +0 -0
  75. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_glm53.py +0 -0
  76. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_glm5v_turbo.py +0 -0
  77. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_gpt3_5.py +0 -0
  78. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_gpt4_1.py +0 -0
  79. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_gpt4o.py +0 -0
  80. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_gpt5.py +0 -0
  81. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_gpt6.py +0 -0
  82. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_o1.py +0 -0
  83. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_o3.py +0 -0
  84. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_o4.py +0 -0
  85. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_qwen.py +0 -0
  86. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_qwen3_vl_models.py +0 -0
  87. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_qwen_ollama.py +0 -0
  88. {whosellm-0.2.7 → whosellm-0.3.0}/tests/models/families/test_qwen_plus.py +0 -0
  89. {whosellm-0.2.7 → whosellm-0.3.0}/tests/test_auto_register.py +0 -0
  90. {whosellm-0.2.7 → whosellm-0.3.0}/tests/test_claude_4x_versions.py +0 -0
  91. {whosellm-0.2.7 → whosellm-0.3.0}/tests/test_llmeta.py +0 -0
  92. {whosellm-0.2.7 → whosellm-0.3.0}/tests/test_model_version.py +0 -0
  93. {whosellm-0.2.7 → whosellm-0.3.0}/tests/test_provider.py +0 -0
  94. {whosellm-0.2.7 → whosellm-0.3.0}/tests/test_registry_merge.py +0 -0
  95. {whosellm-0.2.7 → whosellm-0.3.0}/tests/test_specific_patterns.py +0 -0
  96. {whosellm-0.2.7 → whosellm-0.3.0}/tests/test_variant_priority_config.py +0 -0
  97. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/model_version.py +0 -0
  98. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/__init__.py +0 -0
  99. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/base.py +0 -0
  100. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/config.py +0 -0
  101. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/dynamic_enum.py +0 -0
  102. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/__init__.py +0 -0
  103. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/deepseek/__init__.py +0 -0
  104. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/deepseek/tencent.py +0 -0
  105. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/__init__.py +0 -0
  106. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_3_5.py +0 -0
  107. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_4.py +0 -0
  108. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_4_1.py +0 -0
  109. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_4o.py +0 -0
  110. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_5.py +0 -0
  111. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_5_1.py +0 -0
  112. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_5_2.py +0 -0
  113. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_5_3.py +0 -0
  114. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_5_4.py +0 -0
  115. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_gpt_6.py +0 -0
  116. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_o1.py +0 -0
  117. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_o3.py +0 -0
  118. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/openai/openai_o4.py +0 -0
  119. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/others.py +0 -0
  120. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/families/vidu.py +0 -0
  121. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/patterns.py +0 -0
  122. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/models/registry.py +0 -0
  123. {whosellm-0.2.7 → whosellm-0.3.0}/whosellm/provider.py +0 -0
  124. {whosellm-0.2.7 → whosellm-0.3.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.2.7"
5
+ current_version = "0.3.0"
6
6
  parse = """(?x)
7
7
  (?P<major>0|[1-9]\\d*)\\.
8
8
  (?P<minor>0|[1-9]\\d*)\\.
@@ -112,6 +112,7 @@ LLMeta("gpt-4o-mini")
112
112
  - **`Provider`**(`provider.py`):动态字符串枚举(OpenAI、Anthropic、Zhipu 等),运行时可通过 `add_member()` 扩展。
113
113
  - **`ModelFamily`**(`models/base.py`):动态字符串枚举,运行时可通过 `add_member()` 扩展。
114
114
  - **`ModelCapabilities`**(`capabilities.py`):冻结 dataclass,描述模型功能(thinking、vision、audio、video、function_calling 等)和限制(context_window、max_tokens 等)。
115
+ - `media_count_limit`(`MediaCountLimit`):单次请求的媒体数量上限。值域:`0`(确认不支持)、正整数(文档化上限)、`math.inf`(`UNLIMITED`,无文档化上限,仅受 token 预算约束);键缺失视同不支持(`limit > 0` 判断支持性,`UNLIMITED` 为 truthy)。`count_mode` 区分 `"per_type"`(各类型独立计数)与 `"combined"`(共享总池,如 GLM 系列)。
115
116
  - `supports_json_outputs`:模型能否将输出限制为合法 JSON(`response_format: {type: "json_object"}`),但**不约束 JSON 结构**。
116
117
  - `supports_structured_outputs`:模型能否按调用方提供的 **JSON Schema** 生成严格符合结构的输出(`response_format: {type: "json_schema", ...}`)。这是更强的能力,蕴含 `supports_json_outputs`。
117
118
  - **`ModelFamilyConfig`**(`models/config.py`):配置 dataclass,定义模式、默认值和特定模型覆盖。创建时自动注册到全局注册表。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: whosellm
3
- Version: 0.2.7
3
+ Version: 0.3.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "whosellm"
3
- version = "0.2.7"
3
+ version = "0.3.0"
4
4
  description = "A unified LLM model version and capability management library"
5
5
  authors = [
6
6
  {name = "JQQ", email = "jqq1716@gmail.com"}
@@ -233,7 +233,9 @@ CLAUDE_30_MODELS = [
233
233
  # 聚合 + 参数化
234
234
  # ============================================================================
235
235
 
236
- ALL_MODELS = CLAUDE_51_MODELS + CLAUDE_LATEST_MODELS + CLAUDE_45_MODELS + CLAUDE_41_MODELS + CLAUDE_40_MODELS + CLAUDE_30_MODELS
236
+ ALL_MODELS = (
237
+ CLAUDE_51_MODELS + CLAUDE_LATEST_MODELS + CLAUDE_45_MODELS + CLAUDE_41_MODELS + CLAUDE_40_MODELS + CLAUDE_30_MODELS
238
+ )
237
239
 
238
240
 
239
241
  @pytest.mark.e2e
@@ -0,0 +1,74 @@
1
+ # filename: test_media_count_limit_config.py
2
+ """
3
+ 各家族媒体数量限制的集成断言(官方文档来源见配置注释)
4
+ Per-family media count limit integration tests (official docs cited in config comments)
5
+ """
6
+
7
+ import math
8
+
9
+ from whosellm import UNLIMITED
10
+ from whosellm.model_version import LLMeta
11
+
12
+ OFFICIAL = (
13
+ # (模型名, 官方图片数量上限/请求, 计数模式)
14
+ # 来源 / Sources:
15
+ # Anthropic: platform.claude.com/docs/en/build-with-claude/vision (600/1M ctx, 100/200k ctx)
16
+ # OpenAI: developers.openai.com/api/docs/guides/images-vision (1,500)
17
+ # Gemini: ai.google.dev/gemini-api/docs/image-understanding (3,600)
18
+ # Zhipu: docs.bigmodel.cn(GLM-5.3-Flash 未公开上限)/ zai-org/GLM-skills(GLM-4.6v ≤50)
19
+ # DeepSeek: api-docs.deepseek.com/guides/vision (600)
20
+ # Alibaba: help.aliyun.com/zh/model-studio/vision (256 URL / 250 base64)
21
+ ("claude-fable-5-1", 600, "per_type"),
22
+ ("claude-mythos-5-1", 600, "per_type"),
23
+ ("claude-opus-5", 600, "per_type"),
24
+ ("claude-sonnet-5", 600, "per_type"),
25
+ ("claude-opus-4-8", 600, "per_type"),
26
+ ("claude-sonnet-4-6", 600, "per_type"),
27
+ ("gpt-5.6", 1500, "per_type"),
28
+ ("gpt-5.5", 1500, "per_type"),
29
+ ("gemini-3.8-flash", 3600, "per_type"),
30
+ ("gemini-3.7-flash", 3600, "per_type"),
31
+ ("glm-4.6v", 50, "combined"),
32
+ ("glm-4.6v-flash", 50, "combined"),
33
+ ("deepseek-v4-flash-vision-exp", 600, "per_type"),
34
+ ("qwen3-vl-plus", 256, "per_type"),
35
+ ("qwen3-vl-flash", 256, "per_type"),
36
+ )
37
+
38
+
39
+ class TestLatestModels:
40
+ """最新/次新模型的官方图片数量上限 / Official image count caps for latest models"""
41
+
42
+ def test_official_image_caps(self):
43
+ for model_name, expected, mode in OFFICIAL:
44
+ mcl = LLMeta(model_name).capabilities.media_count_limit
45
+ assert mcl.get("image") == expected, f"{model_name}: {mcl.get('image')} != {expected}"
46
+ assert mcl.count_mode == mode, f"{model_name}: count_mode mismatch"
47
+ assert mcl.is_supported("image") is True
48
+
49
+ def test_glm_combined_shared_pool(self):
50
+ """GLM 系列为合并计数:图片/视频等素材共享 50 的总池"""
51
+ for model_name in ("glm-4.6v", "glm-4.6v-flash"):
52
+ mcl = LLMeta(model_name).capabilities.media_count_limit
53
+ assert mcl.count_mode == "combined"
54
+ assert mcl.combined == 50
55
+
56
+ def test_glm_5v_turbo_no_documented_cap(self):
57
+ """GLM-5.3-Flash(glm-5v-turbo)官方未公开数量上限 → UNLIMITED"""
58
+ mcl = LLMeta("glm-5v-turbo").capabilities.media_count_limit
59
+ assert mcl.get("image") is math.inf
60
+ assert mcl.get("image") is UNLIMITED
61
+
62
+
63
+ class TestUnmeasuredModels:
64
+ """未实测模型:键缺失视同不支持 / Unmeasured models: missing keys treated as unsupported"""
65
+
66
+ def test_200k_claude_without_specific_cap(self):
67
+ """老代模型未采集 → image 为 0(视同不支持),不误报为无上限"""
68
+ mcl = LLMeta("claude-3-5-haiku").capabilities.media_count_limit
69
+ assert mcl.get("image") == 0
70
+ assert bool(mcl.get("image")) is False
71
+
72
+ def test_text_only_model(self):
73
+ mcl = LLMeta("deepseek-v4-flash").capabilities.media_count_limit
74
+ assert mcl.get("image") == 0
@@ -0,0 +1,114 @@
1
+ # filename: test_media_count_limit.py
2
+ """
3
+ MediaCountLimit 单元测试 / MediaCountLimit unit tests
4
+
5
+ 值域约定 / Value domain:
6
+ - 0 确认不支持 / confirmed unsupported
7
+ - 正整数 文档化上限 / documented cap
8
+ - UNLIMITED 无文档化上限,仅受上下文 token 预算约束 / no documented cap (math.inf)
9
+ - 键缺失 = 未实测,视同不支持 / missing key = unmeasured, treated as unsupported
10
+ """
11
+
12
+ import math
13
+ from dataclasses import FrozenInstanceError
14
+
15
+ import pytest
16
+
17
+ from whosellm import UNLIMITED, MediaCountLimit
18
+ from whosellm.capabilities import ModelCapabilities
19
+
20
+
21
+ class TestDefault:
22
+ """默认行为 / Default behavior"""
23
+
24
+ def test_default_per_type_empty(self):
25
+ mcl = MediaCountLimit()
26
+ assert mcl.per_type == {}
27
+ assert mcl.count_mode == "per_type"
28
+ assert mcl.combined is None
29
+
30
+ def test_missing_key_returns_zero(self):
31
+ """键缺失(未实测)视同不支持 / Missing keys are treated as unsupported"""
32
+ mcl = MediaCountLimit(per_type={"image": 10})
33
+ assert mcl.get("video") == 0
34
+ assert mcl.is_supported("video") is False
35
+
36
+ def test_model_capabilities_default(self):
37
+ caps = ModelCapabilities()
38
+ assert caps.media_count_limit == MediaCountLimit()
39
+ assert caps.media_count_limit.get("image") == 0
40
+
41
+
42
+ class TestValueDomain:
43
+ """取值域语义 / Value domain semantics: 0 / int / UNLIMITED"""
44
+
45
+ def test_zero_means_unsupported(self):
46
+ mcl = MediaCountLimit(per_type={"image": 0})
47
+ assert mcl.get("image") == 0
48
+ assert mcl.is_supported("image") is False
49
+ assert bool(mcl.get("image")) is False
50
+
51
+ def test_positive_int_is_documented_cap(self):
52
+ mcl = MediaCountLimit(per_type={"image": 600})
53
+ assert mcl.get("image") == 600
54
+ assert mcl.is_supported("image") is True
55
+ assert bool(mcl.get("image")) is True
56
+
57
+ def test_unlimited_is_truthy(self):
58
+ """UNLIMITED 必须是 truthy——这是 0 与"无上限"在 bool 上下文的唯一区分点"""
59
+ mcl = MediaCountLimit(per_type={"image": UNLIMITED})
60
+ assert mcl.get("image") is math.inf
61
+ assert mcl.is_supported("image") is True
62
+ assert bool(mcl.get("image")) is True
63
+
64
+ def test_bool_never_conflates_zero_and_unlimited(self):
65
+ assert bool(0) is False
66
+ assert bool(UNLIMITED) is True
67
+
68
+ def test_unlimited_comparison_with_positive_ints(self):
69
+ assert UNLIMITED > 50
70
+ assert UNLIMITED > 0
71
+ assert not UNLIMITED < 600
72
+
73
+ def test_is_supported_uses_positive_check(self):
74
+ """is_supported 基于 limit > 0,区分 0 / 正整数 / UNLIMITED"""
75
+ assert MediaCountLimit(per_type={"image": 0}).is_supported("image") is False
76
+ assert MediaCountLimit(per_type={"image": 1}).is_supported("image") is True
77
+ assert MediaCountLimit(per_type={"image": UNLIMITED}).is_supported("image") is True
78
+
79
+
80
+ class TestCountingMode:
81
+ """计数模式 / Counting mode"""
82
+
83
+ def test_per_type_mode_default(self):
84
+ mcl = MediaCountLimit(per_type={"image": 600})
85
+ assert mcl.count_mode == "per_type"
86
+ assert mcl.combined is None
87
+
88
+ def test_combined_mode(self):
89
+ mcl = MediaCountLimit(per_type={"image": 50, "video": 50}, count_mode="combined", combined=50)
90
+ assert mcl.count_mode == "combined"
91
+ assert mcl.combined == 50
92
+ assert mcl.get("image") == 50
93
+
94
+
95
+ class TestImmutability:
96
+ """不可变性 / Immutability (frozen dataclass)
97
+
98
+ 注意:frozen 为浅冻结——字段引用不可重绑,与项目内既有 list 字段
99
+ (supported_image_mime_type 等)的语义保持一致。
100
+ Note: frozen is shallow — field rebinding is rejected, same semantics as
101
+ existing list fields (supported_image_mime_type etc.).
102
+ """
103
+
104
+ def test_field_reassignment_rejected(self):
105
+ mcl = MediaCountLimit(per_type={"image": 50})
106
+ with pytest.raises(FrozenInstanceError):
107
+ mcl.per_type = {} # type: ignore[misc]
108
+
109
+ def test_default_factory_isolation(self):
110
+ """default_factory 保证每个实例独立,不共享可变状态"""
111
+ a = ModelCapabilities()
112
+ b = ModelCapabilities()
113
+ assert a.media_count_limit is not b.media_count_limit
114
+ assert a.media_count_limit.per_type is not b.media_count_limit.per_type
@@ -968,7 +968,7 @@ wheels = [
968
968
 
969
969
  [[package]]
970
970
  name = "whosellm"
971
- version = "0.2.7"
971
+ version = "0.3.0"
972
972
  source = { editable = "." }
973
973
  dependencies = [
974
974
  { name = "parse" },
@@ -7,15 +7,17 @@
7
7
  LLMeta - 统一的大语言模型版本和能力管理库 / A unified LLM model version and capability management library
8
8
  """
9
9
 
10
- __version__ = "0.2.7"
10
+ __version__ = "0.3.0"
11
11
 
12
- from whosellm.capabilities import ModelCapabilities
12
+ from whosellm.capabilities import UNLIMITED, MediaCountLimit, ModelCapabilities
13
13
  from whosellm.model_version import LLMeta
14
14
  from whosellm.models.base import ModelFamily
15
15
  from whosellm.provider import Provider
16
16
 
17
17
  __all__ = [
18
+ "UNLIMITED",
18
19
  "LLMeta",
20
+ "MediaCountLimit",
19
21
  "ModelCapabilities",
20
22
  "ModelFamily",
21
23
  "Provider",
@@ -7,7 +7,52 @@
7
7
  模型能力定义 / Model capability definitions
8
8
  """
9
9
 
10
+ import math
10
11
  from dataclasses import dataclass, field
12
+ from typing import Literal
13
+
14
+ # 无文档化上限哨兵:该媒体类型官方未给出数量上限,仅受上下文 token 预算约束
15
+ # Sentinel for "no documented limit": the provider documents no count cap,
16
+ # only the context-window token budget constrains the input.
17
+ UNLIMITED = math.inf
18
+
19
+ MediaCount = int | float # 正整数 = 文档化上限;float 仅允许 UNLIMITED(math.inf)
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class MediaCountLimit:
24
+ """
25
+ 单次请求媒体数量上限 / Per-request media count limits
26
+
27
+ 值域 / Value domain(键缺失 = 未实测,视同不支持):
28
+ - 0 确认不支持 / confirmed unsupported
29
+ - 正整数 文档化上限,单位:个(张/段/...)/ documented cap (count)
30
+ - UNLIMITED 无文档化上限,仅受上下文 token 预算约束 / no documented cap
31
+
32
+ count_mode:
33
+ - "per_type": 各类型独立计数(如 "image": 600 仅约束图片)/ per-type caps
34
+ - "combined": 所有媒体共享一个总池(如 GLM 系列图片+视频合并计数)/ shared pool
35
+ """
36
+
37
+ per_type: dict[str, MediaCount] = field(default_factory=dict)
38
+ count_mode: Literal["per_type", "combined"] = "per_type"
39
+ # combined 模式的共享上限(per_type 模式下应为 None)
40
+ # Shared pool cap for "combined" mode (None when count_mode == "per_type")
41
+ combined: MediaCount | None = None
42
+
43
+ def get(self, media_type: str) -> MediaCount:
44
+ """
45
+ 返回该媒体类型的上限;键缺失(未实测)返回 0(视同不支持)
46
+ Return the cap for a media type; missing keys (unmeasured) return 0 (treated as unsupported)
47
+ """
48
+ return self.per_type.get(media_type, 0)
49
+
50
+ def is_supported(self, media_type: str) -> bool:
51
+ """
52
+ 该媒体类型是否支持(0 > 0 为 False,正整数与 UNLIMITED 均为 True)
53
+ Whether the media type is supported at all
54
+ """
55
+ return self.get(media_type) > 0
11
56
 
12
57
 
13
58
  @dataclass(frozen=True)
@@ -51,6 +96,9 @@ class ModelCapabilities:
51
96
  ) # 支持的图片MIME类型 / Supported image MIME types
52
97
  supports_image_base64: bool = True # 是否支持base64编码的图片 / Whether base64-encoded images are supported
53
98
 
99
+ # 单次请求媒体(图片/视频/音频)数量限制 / Per-request media count limits
100
+ media_count_limit: MediaCountLimit = field(default_factory=MediaCountLimit)
101
+
54
102
  # 视频相关限制 / Video-related limitations
55
103
  max_video_size_mb: float | None = None # 最大视频大小(MB) / Maximum video size in MB
56
104
  max_video_duration_seconds: int | None = None # 最大视频时长(秒) / Maximum video duration in seconds
@@ -7,7 +7,7 @@
7
7
  阿里巴巴模型家族配置 / Alibaba model family configurations
8
8
  """
9
9
 
10
- from whosellm.capabilities import ModelCapabilities
10
+ from whosellm.capabilities import MediaCountLimit, ModelCapabilities
11
11
  from whosellm.models.base import ModelFamily
12
12
  from whosellm.models.config import ModelFamilyConfig, SpecificModelConfig
13
13
  from whosellm.provider import Provider
@@ -142,6 +142,9 @@ QWEN = ModelFamilyConfig(
142
142
  supports_structured_outputs=True,
143
143
  max_tokens=32000,
144
144
  context_window=256000,
145
+ # 官方:公网 URL/本地路径最多 256 张;Base64 250 张(help.aliyun.com/zh/model-studio/vision)
146
+ # Per docs: 256 images via URL/local path; 250 via base64
147
+ media_count_limit=MediaCountLimit(per_type={"image": 256}),
145
148
  ),
146
149
  patterns=[
147
150
  "qwen3-vl-plus-{year:4d}-{month:2d}-{day:2d}",
@@ -213,6 +216,9 @@ QWEN = ModelFamilyConfig(
213
216
  supports_structured_outputs=True,
214
217
  max_tokens=32000,
215
218
  context_window=256000,
219
+ # 官方:公网 URL/本地路径最多 256 张;Base64 250 张(help.aliyun.com/zh/model-studio/vision)
220
+ # Per docs: 256 images via URL/local path; 250 via base64
221
+ media_count_limit=MediaCountLimit(per_type={"image": 256}),
216
222
  ),
217
223
  patterns=[
218
224
  "qwen3-vl-flash-{year:4d}-{month:2d}-{day:2d}",
@@ -7,7 +7,7 @@
7
7
  Anthropic 模型家族配置 / Anthropic model family configurations
8
8
  """
9
9
 
10
- from whosellm.capabilities import ModelCapabilities
10
+ from whosellm.capabilities import MediaCountLimit, ModelCapabilities
11
11
  from whosellm.models.base import ModelFamily
12
12
  from whosellm.models.config import ModelFamilyConfig, SpecificModelConfig
13
13
  from whosellm.provider import Provider
@@ -41,6 +41,9 @@ CLAUDE = ModelFamilyConfig(
41
41
  supports_streaming=True,
42
42
  max_tokens=64000,
43
43
  context_window=200000,
44
+ # 200k 上下文模型:官方上限 100 张/请求;1M 模型为 600(见各 specific_models)
45
+ # 200k-context models: 100 images/request per docs; 1M models: 600 (see specific_models)
46
+ media_count_limit=MediaCountLimit(per_type={"image": 100}),
44
47
  ),
45
48
  specific_models={
46
49
  # Mythos-class(2026-06-09 发布):全新顶级层级,优先级高于 opus
@@ -59,6 +62,9 @@ CLAUDE = ModelFamilyConfig(
59
62
  supports_computer_use=True,
60
63
  max_tokens=128000,
61
64
  context_window=1000000,
65
+ # 官方上限:1M 上下文模型最多 600 张图片/请求(https://docs.anthropic.com/en/docs/build-with-claude/vision)
66
+ # Per docs: up to 600 images per request for 1M-context models
67
+ media_count_limit=MediaCountLimit(per_type={"image": 600}),
62
68
  ),
63
69
  patterns=[
64
70
  "claude-fable-5-{snapshot:snapshot}",
@@ -79,6 +85,9 @@ CLAUDE = ModelFamilyConfig(
79
85
  supports_computer_use=True,
80
86
  max_tokens=128000,
81
87
  context_window=1000000,
88
+ # 官方上限:1M 上下文模型最多 600 张图片/请求(https://docs.anthropic.com/en/docs/build-with-claude/vision)
89
+ # Per docs: up to 600 images per request for 1M-context models
90
+ media_count_limit=MediaCountLimit(per_type={"image": 600}),
82
91
  ),
83
92
  patterns=[
84
93
  "claude-mythos-5-{snapshot:snapshot}",
@@ -103,6 +112,9 @@ CLAUDE = ModelFamilyConfig(
103
112
  supports_computer_use=True,
104
113
  max_tokens=128000,
105
114
  context_window=1000000,
115
+ # 官方上限:1M 上下文模型最多 600 张图片/请求(https://docs.anthropic.com/en/docs/build-with-claude/vision)
116
+ # Per docs: up to 600 images per request for 1M-context models
117
+ media_count_limit=MediaCountLimit(per_type={"image": 600}),
106
118
  ),
107
119
  patterns=[
108
120
  "claude-fable-5-1-{snapshot:snapshot}",
@@ -125,6 +137,9 @@ CLAUDE = ModelFamilyConfig(
125
137
  supports_computer_use=True,
126
138
  max_tokens=128000,
127
139
  context_window=1000000,
140
+ # 官方上限:1M 上下文模型最多 600 张图片/请求(https://docs.anthropic.com/en/docs/build-with-claude/vision)
141
+ # Per docs: up to 600 images per request for 1M-context models
142
+ media_count_limit=MediaCountLimit(per_type={"image": 600}),
128
143
  ),
129
144
  patterns=[
130
145
  "claude-mythos-5-1-{snapshot:snapshot}",
@@ -149,6 +164,9 @@ CLAUDE = ModelFamilyConfig(
149
164
  supports_computer_use=True,
150
165
  max_tokens=128000,
151
166
  context_window=1000000,
167
+ # 官方上限:1M 上下文模型最多 600 张图片/请求(https://docs.anthropic.com/en/docs/build-with-claude/vision)
168
+ # Per docs: up to 600 images per request for 1M-context models
169
+ media_count_limit=MediaCountLimit(per_type={"image": 600}),
152
170
  ),
153
171
  patterns=[
154
172
  "claude-opus-5-{snapshot:snapshot}",
@@ -169,6 +187,9 @@ CLAUDE = ModelFamilyConfig(
169
187
  supports_computer_use=True,
170
188
  max_tokens=128000,
171
189
  context_window=1000000,
190
+ # 官方上限:1M 上下文模型最多 600 张图片/请求(https://docs.anthropic.com/en/docs/build-with-claude/vision)
191
+ # Per docs: up to 600 images per request for 1M-context models
192
+ media_count_limit=MediaCountLimit(per_type={"image": 600}),
172
193
  ),
173
194
  patterns=[
174
195
  "claude-sonnet-5-{snapshot:snapshot}",
@@ -189,6 +210,9 @@ CLAUDE = ModelFamilyConfig(
189
210
  supports_computer_use=True,
190
211
  max_tokens=128000,
191
212
  context_window=1000000,
213
+ # 官方上限:1M 上下文模型最多 600 张图片/请求(https://docs.anthropic.com/en/docs/build-with-claude/vision)
214
+ # Per docs: up to 600 images per request for 1M-context models
215
+ media_count_limit=MediaCountLimit(per_type={"image": 600}),
192
216
  ),
193
217
  patterns=[
194
218
  "claude-opus-4-8-{snapshot:snapshot}",
@@ -209,6 +233,9 @@ CLAUDE = ModelFamilyConfig(
209
233
  supports_computer_use=True,
210
234
  max_tokens=128000,
211
235
  context_window=1000000,
236
+ # 官方上限:1M 上下文模型最多 600 张图片/请求(https://docs.anthropic.com/en/docs/build-with-claude/vision)
237
+ # Per docs: up to 600 images per request for 1M-context models
238
+ media_count_limit=MediaCountLimit(per_type={"image": 600}),
212
239
  ),
213
240
  patterns=[
214
241
  "claude-opus-4-7-{snapshot:snapshot}",
@@ -229,6 +256,9 @@ CLAUDE = ModelFamilyConfig(
229
256
  supports_computer_use=True,
230
257
  max_tokens=128000,
231
258
  context_window=1000000,
259
+ # 官方上限:1M 上下文模型最多 600 张图片/请求(https://docs.anthropic.com/en/docs/build-with-claude/vision)
260
+ # Per docs: up to 600 images per request for 1M-context models
261
+ media_count_limit=MediaCountLimit(per_type={"image": 600}),
232
262
  ),
233
263
  patterns=[
234
264
  "claude-opus-4-6-{snapshot:snapshot}",
@@ -249,6 +279,9 @@ CLAUDE = ModelFamilyConfig(
249
279
  supports_computer_use=True,
250
280
  max_tokens=64000,
251
281
  context_window=1000000,
282
+ # 官方上限:1M 上下文模型最多 600 张图片/请求(https://docs.anthropic.com/en/docs/build-with-claude/vision)
283
+ # Per docs: up to 600 images per request for 1M-context models
284
+ media_count_limit=MediaCountLimit(per_type={"image": 600}),
252
285
  ),
253
286
  patterns=[
254
287
  "claude-sonnet-4-6-{snapshot:snapshot}",
@@ -16,7 +16,7 @@ Legacy aliases: deepseek-chat / deepseek-reasoner (announced to be deprecated;
16
16
  currently map to deepseek-v4-flash non-thinking and thinking modes respectively).
17
17
  """
18
18
 
19
- from whosellm.capabilities import ModelCapabilities
19
+ from whosellm.capabilities import MediaCountLimit, ModelCapabilities
20
20
  from whosellm.models.base import ModelFamily
21
21
  from whosellm.models.config import ModelFamilyConfig, SpecificModelConfig
22
22
  from whosellm.provider import Provider
@@ -93,6 +93,9 @@ DEEPSEEK = ModelFamilyConfig(
93
93
  supports_structured_outputs=False,
94
94
  max_tokens=384_000,
95
95
  context_window=1_000_000,
96
+ # 官方文档:单请求最多 600 张图片;≥15 张时单图长边降至 4096px(api-docs.deepseek.com/guides/vision)
97
+ # Per docs: up to 600 images per request; per-image long edge drops to 4096px with 15+ images
98
+ media_count_limit=MediaCountLimit(per_type={"image": 600}),
96
99
  ),
97
100
  patterns=[
98
101
  "deepseek-v4-flash-vision-exp",
@@ -7,7 +7,7 @@
7
7
  Google Gemini 模型家族配置 / Google Gemini model family configurations
8
8
  """
9
9
 
10
- from whosellm.capabilities import ModelCapabilities
10
+ from whosellm.capabilities import MediaCountLimit, ModelCapabilities
11
11
  from whosellm.models.base import ModelFamily
12
12
  from whosellm.models.config import ModelFamilyConfig, SpecificModelConfig
13
13
  from whosellm.provider import Provider
@@ -436,6 +436,9 @@ GEMINI = ModelFamilyConfig(
436
436
  supports_code_interpreter=True,
437
437
  max_tokens=65536,
438
438
  context_window=1048576,
439
+ # 官方文档:单请求最多 3600 张图片(https://ai.google.dev/gemini-api/docs/image-understanding)
440
+ # Per docs: up to 3,600 image files per request
441
+ media_count_limit=MediaCountLimit(per_type={"image": 3600}),
439
442
  ),
440
443
  patterns=["gemini-3.7-flash"],
441
444
  ),
@@ -460,6 +463,9 @@ GEMINI = ModelFamilyConfig(
460
463
  supports_code_interpreter=True,
461
464
  max_tokens=65536,
462
465
  context_window=1048576,
466
+ # 官方文档:单请求最多 3600 张图片(https://ai.google.dev/gemini-api/docs/image-understanding)
467
+ # Per docs: up to 3,600 image files per request
468
+ media_count_limit=MediaCountLimit(per_type={"image": 3600}),
463
469
  ),
464
470
  patterns=["gemini-3.8-flash"],
465
471
  ),
@@ -1,4 +1,4 @@
1
- from whosellm.capabilities import ModelCapabilities
1
+ from whosellm.capabilities import MediaCountLimit, ModelCapabilities
2
2
  from whosellm.models.base import ModelFamily
3
3
  from whosellm.models.config import ModelFamilyConfig, SpecificModelConfig
4
4
  from whosellm.provider import Provider
@@ -33,6 +33,9 @@ GPT_5_5 = ModelFamilyConfig(
33
33
  supports_computer_use=True,
34
34
  max_tokens=128_000,
35
35
  context_window=1_050_000,
36
+ # 官方文档:单请求最多 1500 张图片(https://developers.openai.com/api/docs/guides/images-vision)
37
+ # Per docs: up to 1,500 images per request
38
+ media_count_limit=MediaCountLimit(per_type={"image": 1500}),
36
39
  ),
37
40
  specific_models={
38
41
  "gpt-5.5": SpecificModelConfig(
@@ -1,4 +1,4 @@
1
- from whosellm.capabilities import ModelCapabilities
1
+ from whosellm.capabilities import MediaCountLimit, ModelCapabilities
2
2
  from whosellm.models.base import ModelFamily
3
3
  from whosellm.models.config import ModelFamilyConfig, SpecificModelConfig
4
4
  from whosellm.provider import Provider
@@ -40,6 +40,9 @@ GPT_5_6 = ModelFamilyConfig(
40
40
  supports_computer_use=True,
41
41
  max_tokens=128_000,
42
42
  context_window=1_050_000,
43
+ # 官方文档:单请求最多 1500 张图片(https://developers.openai.com/api/docs/guides/images-vision)
44
+ # Per docs: up to 1,500 images per request
45
+ media_count_limit=MediaCountLimit(per_type={"image": 1500}),
43
46
  ),
44
47
  specific_models={
45
48
  "gpt-5.6": SpecificModelConfig(
@@ -7,7 +7,7 @@
7
7
  智谱 AI 模型家族配置 / Zhipu AI model family configurations
8
8
  """
9
9
 
10
- from whosellm.capabilities import ModelCapabilities
10
+ from whosellm.capabilities import UNLIMITED, MediaCountLimit, ModelCapabilities
11
11
  from whosellm.models.base import ModelFamily
12
12
  from whosellm.models.config import ModelFamilyConfig, SpecificModelConfig
13
13
  from whosellm.provider import Provider
@@ -94,6 +94,9 @@ GLM_VISION = ModelFamilyConfig(
94
94
  supports_streaming=True,
95
95
  max_tokens=128000,
96
96
  context_window=200000,
97
+ # 官方文档未公开单次请求图片数量上限,仅受 1M 上下文 token 预算约束
98
+ # No documented per-request image cap (only the context-window token budget)
99
+ media_count_limit=MediaCountLimit(per_type={"image": UNLIMITED}),
97
100
  ),
98
101
  patterns=[
99
102
  "glm-5v-turbo-{year:4d}-{month:2d}-{day:2d}",
@@ -210,6 +213,9 @@ GLM_VISION = ModelFamilyConfig(
210
213
  supports_streaming=True,
211
214
  max_tokens=128000,
212
215
  context_window=128000,
216
+ # 官方:单请求最多 50 张图片(zai-org/GLM-skills,图片+视频等素材总量上限)
217
+ # Per official GLM-skills: up to 50 images per request (combined with other media)
218
+ media_count_limit=MediaCountLimit(per_type={"image": 50}, count_mode="combined", combined=50),
213
219
  ),
214
220
  ),
215
221
  "glm-4.6v-flash": SpecificModelConfig(
@@ -227,6 +233,9 @@ GLM_VISION = ModelFamilyConfig(
227
233
  supports_streaming=True,
228
234
  max_tokens=128000,
229
235
  context_window=128000,
236
+ # 官方:单请求最多 50 张图片(zai-org/GLM-skills,图片+视频等素材总量上限)
237
+ # Per official GLM-skills: up to 50 images per request (combined with other media)
238
+ media_count_limit=MediaCountLimit(per_type={"image": 50}, count_mode="combined", combined=50),
230
239
  ),
231
240
  patterns=[
232
241
  "glm-4.6v-flash-{year:4d}-{month:2d}-{day:2d}",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes