api-key-manager 2.1.2__tar.gz → 2.2.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 (103) hide show
  1. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/PKG-INFO +16 -1
  2. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/README.md +15 -0
  3. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/PKG-INFO +16 -1
  4. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/SOURCES.txt +4 -0
  5. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/__init__.py +1 -1
  6. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/detector.py +37 -9
  7. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/__init__.py +9 -1
  8. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/ai302.py +0 -33
  9. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/baichuan.py +0 -28
  10. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/base.py +69 -31
  11. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/cerebras.py +0 -33
  12. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/cstcloud.py +0 -28
  13. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/dashscope.py +0 -28
  14. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/dashscope_coding.py +0 -28
  15. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/deepseek.py +0 -33
  16. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/dmxapi.py +0 -33
  17. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/doubao.py +0 -33
  18. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/fireworks.py +0 -33
  19. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/grok.py +0 -34
  20. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/groq.py +0 -33
  21. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/huggingface.py +0 -17
  22. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/hyperbolic.py +0 -33
  23. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/infini.py +0 -33
  24. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/infini_coding.py +96 -124
  25. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/kimi.py +0 -28
  26. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/kimi_coding.py +96 -124
  27. api_key_manager-2.2.0/key_manager/providers/longcat.py +76 -0
  28. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/mimo.py +0 -33
  29. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/mimo_plan.py +0 -33
  30. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/minimax.py +0 -28
  31. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/minimax_plan.py +0 -28
  32. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/mistral.py +0 -33
  33. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/models_registry.py +80 -19
  34. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/modelscope.py +0 -14
  35. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/nvidia.py +0 -33
  36. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/ocoolai.py +0 -33
  37. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/openai.py +0 -39
  38. api_key_manager-2.2.0/key_manager/providers/opencode.py +89 -0
  39. api_key_manager-2.2.0/key_manager/providers/opencode_zen.py +89 -0
  40. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/openrouter.py +0 -28
  41. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/perplexity.py +0 -34
  42. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/poe.py +0 -33
  43. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/ppio.py +0 -33
  44. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/replicate.py +0 -17
  45. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/siliconflow.py +0 -28
  46. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/stepfun.py +0 -33
  47. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/tencent_hunyuan.py +0 -28
  48. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/together.py +0 -33
  49. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/yi.py +0 -28
  50. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/zai.py +0 -33
  51. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/zhipu.py +0 -28
  52. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/zhipu_coding.py +96 -124
  53. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/web.py +11 -9
  54. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/pyproject.toml +1 -1
  55. api_key_manager-2.2.0/tests/test_api_endpoints.py +500 -0
  56. api_key_manager-2.2.0/tests/test_base_check.py +255 -0
  57. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_provider_detection.py +10 -2
  58. api_key_manager-2.1.2/key_manager/providers/longcat.py +0 -123
  59. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/dependency_links.txt +0 -0
  60. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/entry_points.txt +0 -0
  61. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/requires.txt +0 -0
  62. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/top_level.txt +0 -0
  63. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/__main__.py +0 -0
  64. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/api_models.py +0 -0
  65. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/checker.py +0 -0
  66. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/cli.py +0 -0
  67. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/config.py +0 -0
  68. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/core.py +0 -0
  69. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/errors.py +0 -0
  70. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/i18n.py +0 -0
  71. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/logger.py +0 -0
  72. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/model_capabilities.py +0 -0
  73. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/parser.py +0 -0
  74. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/anthropic.py +0 -0
  75. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/cohere.py +0 -0
  76. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/google.py +0 -0
  77. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/proxy.py +0 -0
  78. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/py.typed +0 -0
  79. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/ssrf.py +0 -0
  80. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/storage.py +0 -0
  81. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/tester.py +0 -0
  82. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/url_override.py +0 -0
  83. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/validator.py +0 -0
  84. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/webhook.py +0 -0
  85. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/setup.cfg +0 -0
  86. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_bug_fixes.py +0 -0
  87. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_checker.py +0 -0
  88. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_core.py +0 -0
  89. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_detector.py +0 -0
  90. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_e2e.py +0 -0
  91. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_errors.py +0 -0
  92. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_i18n.py +0 -0
  93. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_logger.py +0 -0
  94. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_openapi.py +0 -0
  95. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_parser.py +0 -0
  96. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_providers.py +0 -0
  97. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_proxy.py +0 -0
  98. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_security.py +0 -0
  99. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_storage.py +0 -0
  100. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_tester.py +0 -0
  101. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_validator.py +0 -0
  102. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_web_fixes.py +0 -0
  103. {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api-key-manager
3
- Version: 2.1.2
3
+ Version: 2.2.0
4
4
  Summary: Batch manage API keys for 44+ AI providers with CLI and Web interfaces
5
5
  Author: Townrain
6
6
  License: MIT
@@ -51,6 +51,11 @@ Requires-Dist: ruff>=0.4.0; extra == "dev"
51
51
  - **SDK 支持** - Python 和 TypeScript 客户端库
52
52
  - **Webhook 通知** - 事件驱动的 Webhook 通知系统
53
53
 
54
+ ## 系统架构
55
+
56
+ ![系统架构流程图](docs/images/flowchart.png)
57
+
58
+
54
59
  ## 支持的 AI 服务商
55
60
 
56
61
  ### 国际
@@ -713,6 +718,16 @@ const result = await client.checkSingleKey({ key: 'sk-xxx', provider: 'openai' }
713
718
 
714
719
  ## 更新日志
715
720
 
721
+ ### v2.2.0 (2026-06-14)
722
+
723
+ - **检测逻辑重构**: 添加三步检测逻辑(/v1/models → 并发测试)
724
+ - **URL 修复**: 从 check_endpoint 提取版本路径,修复 OpenCode 等服务商 404 问题
725
+ - **并发优化**: /v1/models 和 chat/completions 都并发调用
726
+ - **超时控制**: 所有网络请求 5 秒超时
727
+ - **模型同步**: 从 Cherry Studio 同步模型数据,支持 ownedBy 映射
728
+ - **新增服务商**: OpenCode Go、OpenCode Zen
729
+ - **新增服务商**: OpenCode Go、OpenCode Zen
730
+
716
731
  ### v2.1.2 (2026-06-11)
717
732
 
718
733
  - **Bug 修复**: 修复 `KeyManager.detect_provider()` 调用异步函数未 await 的问题
@@ -18,6 +18,11 @@
18
18
  - **SDK 支持** - Python 和 TypeScript 客户端库
19
19
  - **Webhook 通知** - 事件驱动的 Webhook 通知系统
20
20
 
21
+ ## 系统架构
22
+
23
+ ![系统架构流程图](docs/images/flowchart.png)
24
+
25
+
21
26
  ## 支持的 AI 服务商
22
27
 
23
28
  ### 国际
@@ -680,6 +685,16 @@ const result = await client.checkSingleKey({ key: 'sk-xxx', provider: 'openai' }
680
685
 
681
686
  ## 更新日志
682
687
 
688
+ ### v2.2.0 (2026-06-14)
689
+
690
+ - **检测逻辑重构**: 添加三步检测逻辑(/v1/models → 并发测试)
691
+ - **URL 修复**: 从 check_endpoint 提取版本路径,修复 OpenCode 等服务商 404 问题
692
+ - **并发优化**: /v1/models 和 chat/completions 都并发调用
693
+ - **超时控制**: 所有网络请求 5 秒超时
694
+ - **模型同步**: 从 Cherry Studio 同步模型数据,支持 ownedBy 映射
695
+ - **新增服务商**: OpenCode Go、OpenCode Zen
696
+ - **新增服务商**: OpenCode Go、OpenCode Zen
697
+
683
698
  ### v2.1.2 (2026-06-11)
684
699
 
685
700
  - **Bug 修复**: 修复 `KeyManager.detect_provider()` 调用异步函数未 await 的问题
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api-key-manager
3
- Version: 2.1.2
3
+ Version: 2.2.0
4
4
  Summary: Batch manage API keys for 44+ AI providers with CLI and Web interfaces
5
5
  Author: Townrain
6
6
  License: MIT
@@ -51,6 +51,11 @@ Requires-Dist: ruff>=0.4.0; extra == "dev"
51
51
  - **SDK 支持** - Python 和 TypeScript 客户端库
52
52
  - **Webhook 通知** - 事件驱动的 Webhook 通知系统
53
53
 
54
+ ## 系统架构
55
+
56
+ ![系统架构流程图](docs/images/flowchart.png)
57
+
58
+
54
59
  ## 支持的 AI 服务商
55
60
 
56
61
  ### 国际
@@ -713,6 +718,16 @@ const result = await client.checkSingleKey({ key: 'sk-xxx', provider: 'openai' }
713
718
 
714
719
  ## 更新日志
715
720
 
721
+ ### v2.2.0 (2026-06-14)
722
+
723
+ - **检测逻辑重构**: 添加三步检测逻辑(/v1/models → 并发测试)
724
+ - **URL 修复**: 从 check_endpoint 提取版本路径,修复 OpenCode 等服务商 404 问题
725
+ - **并发优化**: /v1/models 和 chat/completions 都并发调用
726
+ - **超时控制**: 所有网络请求 5 秒超时
727
+ - **模型同步**: 从 Cherry Studio 同步模型数据,支持 ownedBy 映射
728
+ - **新增服务商**: OpenCode Go、OpenCode Zen
729
+ - **新增服务商**: OpenCode Go、OpenCode Zen
730
+
716
731
  ### v2.1.2 (2026-06-11)
717
732
 
718
733
  - **Bug 修复**: 修复 `KeyManager.detect_provider()` 调用异步函数未 await 的问题
@@ -62,6 +62,8 @@ key_manager/providers/modelscope.py
62
62
  key_manager/providers/nvidia.py
63
63
  key_manager/providers/ocoolai.py
64
64
  key_manager/providers/openai.py
65
+ key_manager/providers/opencode.py
66
+ key_manager/providers/opencode_zen.py
65
67
  key_manager/providers/openrouter.py
66
68
  key_manager/providers/perplexity.py
67
69
  key_manager/providers/poe.py
@@ -75,6 +77,8 @@ key_manager/providers/yi.py
75
77
  key_manager/providers/zai.py
76
78
  key_manager/providers/zhipu.py
77
79
  key_manager/providers/zhipu_coding.py
80
+ tests/test_api_endpoints.py
81
+ tests/test_base_check.py
78
82
  tests/test_bug_fixes.py
79
83
  tests/test_checker.py
80
84
  tests/test_core.py
@@ -13,5 +13,5 @@ __all__ = [
13
13
  "KeyStore",
14
14
  ]
15
15
 
16
- __version__ = "2.1.2"
16
+ __version__ = "2.2.0"
17
17
 
@@ -223,31 +223,59 @@ async def detect_provider(client, key: str, suspected_provider: str = None) -> s
223
223
  for name, valid in format_results:
224
224
  if valid:
225
225
  return name
226
- # Step 4: Concurrently probe ALL providers with their top 5 models
226
+ # Step 4: Concurrently probe ALL providers
227
+ # First, get models from all providers concurrently
228
+ async def get_provider_models(name, provider):
229
+ """Get models from /v1/models endpoint."""
230
+ try:
231
+ resp = await asyncio.wait_for(
232
+ client.get(
233
+ f"{provider.get_base_url()}{provider.check_endpoint}",
234
+ headers=provider.build_headers(key),
235
+ ),
236
+ timeout=5.0
237
+ )
238
+ if resp.status_code == 200:
239
+ data = resp.json()
240
+ if isinstance(data, dict) and "data" in data:
241
+ models = [m.get("id", "") for m in data["data"] if m.get("id")]
242
+ if models:
243
+ return name, models
244
+ except:
245
+ pass
246
+ return name, []
247
+
248
+ # Get models from all providers concurrently
249
+ model_tasks = [get_provider_models(name, provider) for name, provider in PROVIDERS.items()]
250
+ model_results = await asyncio.gather(*model_tasks)
251
+
227
252
  # Build tasks: (provider_name, model) pairs
228
253
  tasks = []
229
- for name, provider in PROVIDERS.items():
230
- models = PROVIDER_MODELS.get(name, [])
254
+ for name, models in model_results:
231
255
  if not models:
232
- models = [getattr(provider, 'check_model', 'gpt-3.5-turbo')]
233
-
234
- # Use first 5 models
235
- for model in models[:5]:
256
+ continue
257
+ for model in models:
236
258
  tasks.append((name, model))
237
259
 
238
260
  # Concurrently check all (provider, model) pairs
261
+ import re
262
+
239
263
  async def try_model(name, model):
240
264
  provider = PROVIDERS[name]
241
265
  headers = provider.build_headers(key)
242
266
  headers["Content-Type"] = "application/json"
267
+ # Extract version path from check_endpoint
268
+ version_match = re.match(r'(/v\d+)', provider.check_endpoint or '')
269
+ version_prefix = version_match.group(1) if version_match else ''
270
+ chat_url = f"{provider.get_base_url()}{version_prefix}/chat/completions"
243
271
  try:
244
272
  resp = await asyncio.wait_for(
245
273
  client.post(
246
- f"{provider.get_base_url()}/chat/completions",
274
+ chat_url,
247
275
  headers=headers,
248
276
  json={"model": model, "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
249
277
  ),
250
- timeout=10.0
278
+ timeout=5.0
251
279
  )
252
280
  body = resp.text[:500] if resp.text else ""
253
281
  if resp.status_code == 200:
@@ -43,6 +43,8 @@ from .cstcloud import CSTCloudProvider
43
43
  from .zhipu_coding import ZhipuCodingProvider
44
44
  from .kimi_coding import KimiCodingProvider
45
45
  from .infini_coding import InfiniCodingProvider
46
+ from .opencode import OpenCodeGoProvider
47
+ from .opencode_zen import OpenCodeZenProvider
46
48
 
47
49
  # Provider registry
48
50
  PROVIDERS: dict[str, ProviderBase] = {
@@ -90,6 +92,8 @@ PROVIDERS: dict[str, ProviderBase] = {
90
92
  "zhipu-coding": ZhipuCodingProvider(),
91
93
  "kimi-coding": KimiCodingProvider(),
92
94
  "infini-coding": InfiniCodingProvider(),
95
+ "opencode-go": OpenCodeGoProvider(),
96
+ "opencode-zen": OpenCodeZenProvider(),
93
97
  }
94
98
 
95
99
  # Key prefix to provider mapping (for auto-detection)
@@ -113,7 +117,7 @@ KEY_PREFIX_MAP: dict[str, list[str]] = {
113
117
  "AKID": ["cstcloud"],
114
118
  # Generic sk- prefix (must be last - shared by multiple providers)
115
119
  # Excluded: ppio, nvidia, modelscope, ai21 - their /models endpoints don't validate keys
116
- "sk-": ["openai", "deepseek", "together", "fireworks", "perplexity", "dashscope", "kimi", "siliconflow", "cerebras", "hyperbolic", "mimo", "stepfun", "infini", "zai", "ai302", "dmxapi", "ocoolai", "dashscope-coding", "tencent-hunyuan"],
120
+ "sk-": ["openai", "deepseek", "together", "fireworks", "perplexity", "dashscope", "kimi", "siliconflow", "cerebras", "hyperbolic", "mimo", "stepfun", "infini", "zai", "ai302", "dmxapi", "ocoolai", "dashscope-coding", "tencent-hunyuan", "opencode-go", "opencode-zen"],
117
121
  # MiniMax Token Plan keys
118
122
  "sk-cp-": ["minimax-plan", "infini-coding"],
119
123
  # Kimi Coding Plan keys
@@ -228,6 +232,8 @@ PROVIDER_ERROR_SIGNATURES: dict[str, list[str]] = {
228
232
  "google": ["generativelanguage"],
229
233
  # grok: 实际返回 "Incorrect API key provided: sk***45...console.x.ai."
230
234
  "groq": ["groq"],
235
+ "opencode-go": ["opencode.ai", "zen/go", "creditserror", "no payment method"],
236
+ "opencode-zen": ["opencode.ai", "zen/v1", "creditserror", "no payment method"],
231
237
  }
232
238
 
233
239
 
@@ -273,6 +279,8 @@ PROVIDER_WEBSITES: dict[str, dict[str, str]] = {
273
279
  "longcat": {"name": "LongCat", "url": "https://longcat.com", "docs": "https://longcat.com/docs"},
274
280
  "tencent-hunyuan": {"name": "腾讯混元", "url": "https://cloud.tencent.com/product/hunyuan", "docs": "https://cloud.tencent.com/document/product/1729"},
275
281
  "cstcloud": {"name": "中算云", "url": "https://www.cstcloud.com", "docs": "https://www.cstcloud.com/docs"},
282
+ "opencode-go": {"name": "OpenCode Go", "url": "https://opencode.ai", "docs": "https://opencode.ai/docs/zh-cn/go/"},
283
+ "opencode-zen": {"name": "OpenCode Zen", "url": "https://opencode.ai", "docs": "https://opencode.ai/docs/"},
276
284
  "dashscope-coding": {"name": "阿里百炼编程", "url": "https://dashscope.aliyun.com", "docs": "https://help.aliyun.com/zh/dashscope/"},
277
285
  "mimo-plan": {"name": "MiMo 计划版", "url": "https://mimo.xiaomi.com", "docs": "https://mimo.xiaomi.com/docs"},
278
286
  "minimax-plan": {"name": "MiniMax 计划版", "url": "https://platform.minimaxi.com", "docs": "https://platform.minimaxi.com/document"},
@@ -26,39 +26,6 @@ class AI302Provider(ProviderBase):
26
26
  except Exception:
27
27
  return []
28
28
 
29
- async def check(self, client, key: str) -> CheckResult:
30
- """Real usage test - try to make a minimal chat completion request."""
31
- headers = self.build_headers(key)
32
- headers["Content-Type"] = "application/json"
33
- start = time.monotonic()
34
- try:
35
- resp = await client.post(
36
- f"{self.get_base_url()}/chat/completions",
37
- headers=headers,
38
- json={
39
- "model": "gpt-4o-mini",
40
- "messages": [{"role": "user", "content": "hi"}],
41
- "max_tokens": 5
42
- }
43
- )
44
- latency = (time.monotonic() - start) * 1000
45
-
46
- if resp.status_code == 200:
47
- return CheckResult(True, 200, latency, None)
48
- elif resp.status_code in (401, 403):
49
- return CheckResult(False, resp.status_code, latency, "invalid key or forbidden")
50
- elif resp.status_code == 429:
51
- return CheckResult(False, 429, latency, "rate limited")
52
- else:
53
- try:
54
- data = resp.json()
55
- error_msg = data.get("error", {}).get("message", f"status {resp.status_code}")
56
- except:
57
- error_msg = f"status {resp.status_code}"
58
- return CheckResult(False, resp.status_code, latency, error_msg)
59
- except Exception as e:
60
- return CheckResult(False, None, (time.monotonic() - start) * 1000, str(e))
61
-
62
29
  async def test_token_limit(self, client, key: str, token_steps: list[int]) -> TestResult:
63
30
  headers = self.build_headers(key)
64
31
  last_success = None
@@ -7,7 +7,6 @@ class BaichuanProvider(ProviderBase):
7
7
  name = "baichuan"
8
8
  base_url = "https://api.baichuan-ai.com/v1"
9
9
  check_endpoint = "/models"
10
- check_model = "Baichuan4-Turbo"
11
10
 
12
11
  def build_headers(self, key: str) -> dict:
13
12
  return {"Authorization": f"Bearer {key}"}
@@ -24,33 +23,6 @@ class BaichuanProvider(ProviderBase):
24
23
  except Exception:
25
24
  return []
26
25
 
27
- async def check(self, client, key: str) -> CheckResult:
28
- headers = self.build_headers(key)
29
- headers["Content-Type"] = "application/json"
30
- start = time.monotonic()
31
- try:
32
- resp = await client.post(
33
- f"{self.get_base_url()}/chat/completions",
34
- headers=headers,
35
- json={"model": "Baichuan4-Turbo", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
36
- )
37
- latency = (time.monotonic() - start) * 1000
38
- if resp.status_code == 200:
39
- return CheckResult(True, 200, latency, None)
40
- elif resp.status_code in (401, 403):
41
- return CheckResult(False, resp.status_code, latency, "invalid key or forbidden")
42
- elif resp.status_code == 429:
43
- return CheckResult(False, 429, latency, "rate limited")
44
- else:
45
- try:
46
- data = resp.json()
47
- error_msg = data.get("error", {}).get("message", f"status {resp.status_code}")
48
- except:
49
- error_msg = f"status {resp.status_code}"
50
- return CheckResult(False, resp.status_code, latency, error_msg)
51
- except Exception as e:
52
- return CheckResult(False, None, (time.monotonic() - start) * 1000, str(e))
53
-
54
26
  async def test_token_limit(self, client, key: str, token_steps: list[int]) -> TestResult:
55
27
  headers = self.build_headers(key)
56
28
  last_success = None
@@ -131,38 +131,63 @@ class ProviderBase(ABC):
131
131
  ...
132
132
 
133
133
  async def check(self, client, key: str) -> CheckResult:
134
- """Check key validity using multiple models from PROVIDER_MODELS.
135
-
136
- Strategy:
137
- 1. Get models from PROVIDER_MODELS (Cherry Studio sync)
138
- 2. Try first 5 models with /chat/completions
139
- 3. If any succeeds, return valid
140
- 4. If all fail, return last error
141
-
142
- Providers with non-standard APIs (Anthropic, Google, etc.) should override this.
134
+ """Three-step check logic:
135
+ 1. GET /v1/models → get model list (not for validation)
136
+ 2. < 10 models → serial test with /v1/chat/completions
137
+ 3. >= 10 models parallel test with batch_size=10
143
138
  """
139
+ import asyncio
144
140
  import time
145
- from .models_registry import PROVIDER_MODELS
146
141
 
147
142
  headers = self.build_headers(key)
148
143
  headers["Content-Type"] = "application/json"
149
144
 
150
- # Get models for this provider from Cherry Studio sync
151
- models = PROVIDER_MODELS.get(self.name, [])
145
+ # Providers that don't support /v1/models or it doesn't validate key
146
+ SKIP_MODELS_ENDPOINT = {"replicate", "huggingface", "ppio", "nvidia", "modelscope"}
147
+
148
+ # Endpoints that are NOT models endpoints
149
+ NON_MODELS_ENDPOINTS = {"/v1/account", "/api/whoami-v2", "/auth/key", "/v1/check-api-key"}
150
+
151
+ models = []
152
+
153
+ # Step 1: GET /v1/models to get model list (not for validation)
154
+ # Skip if provider is in skip list OR endpoint is not a models endpoint
155
+ skip_models = (
156
+ self.name in SKIP_MODELS_ENDPOINT
157
+ or self.check_endpoint in NON_MODELS_ENDPOINTS
158
+ or not self.check_endpoint
159
+ )
160
+
161
+ if not skip_models:
162
+ try:
163
+ resp = await client.get(
164
+ f"{self.get_base_url()}{self.check_endpoint}",
165
+ headers=self.build_headers(key)
166
+ )
167
+ if resp.status_code == 200:
168
+ data = resp.json()
169
+ if isinstance(data, dict) and "data" in data:
170
+ models = [m.get("id", "") for m in data["data"] if m.get("id")]
171
+ except Exception:
172
+ pass # Ignore errors, fall back to static list
173
+
174
+ # Fallback to check_model if API returned empty
152
175
  if not models:
153
- # Fallback to check_model if no models in registry
154
176
  models = [self.check_model]
155
177
 
156
- # Try first 5 models
157
- test_models = models[:5]
158
- last_error = ""
159
- last_status = None
178
+ # Helper: get chat completions URL
179
+ # Extract version path from check_endpoint (e.g., /v1 from /v1/models)
180
+ import re
181
+ version_match = re.match(r'(/v\d+)', self.check_endpoint or '')
182
+ version_prefix = version_match.group(1) if version_match else ''
183
+ chat_url = f"{self.get_base_url()}{version_prefix}/chat/completions"
160
184
 
161
- for model in test_models:
185
+ # Helper: test single model
186
+ async def test_model(model: str) -> CheckResult:
162
187
  start = time.monotonic()
163
188
  try:
164
189
  resp = await client.post(
165
- f"{self.get_base_url()}/chat/completions",
190
+ chat_url,
166
191
  headers=headers,
167
192
  json={"model": model, "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
168
193
  )
@@ -176,21 +201,34 @@ class ProviderBase(ABC):
176
201
  return CheckResult(False, 429, latency, "rate limited")
177
202
  else:
178
203
  try:
179
- data = resp.json()
180
- last_error = data.get("error", {}).get("message", f"status {resp.status_code}")
204
+ error_msg = resp.json().get("error", {}).get("message", f"status {resp.status_code}")
181
205
  except:
182
- last_error = f"status {resp.status_code}"
183
- last_status = resp.status_code
184
-
185
- # Simplify error for readability
186
- last_error = simplify_error(last_error, resp.status_code)
206
+ error_msg = f"status {resp.status_code}"
207
+ return CheckResult(False, resp.status_code, latency, simplify_error(error_msg, resp.status_code))
187
208
  except Exception as e:
188
- last_error = str(e)
189
- last_status = None
209
+ return CheckResult(False, None, (time.monotonic() - start) * 1000, str(e))
190
210
 
191
- # All models failed
192
- # All models failed
193
- return CheckResult(False, last_status, 0, last_error or "all models failed")
211
+ # Step 2 & 3: Test models with /v1/chat/completions
212
+ if len(models) < 10:
213
+ # Serial test
214
+ for model in models:
215
+ result = await test_model(model)
216
+ if result.valid:
217
+ return result
218
+ # All failed
219
+ return result if models else CheckResult(False, None, 0, "no models available")
220
+ else:
221
+ # Test all models concurrently (no batching)
222
+ tasks = [test_model(m) for m in models]
223
+ results = await asyncio.gather(*tasks)
224
+
225
+ # Return first success or first error
226
+ for result in results:
227
+ if result.valid:
228
+ return result
229
+
230
+ # All failed, return first error
231
+ return results[0] if results else CheckResult(False, None, 0, "no models available")
194
232
  @abstractmethod
195
233
  async def test_token_limit(self, client, key: str,
196
234
  token_steps: list[int]) -> TestResult:
@@ -26,39 +26,6 @@ class CerebrasProvider(ProviderBase):
26
26
  except Exception:
27
27
  return []
28
28
 
29
- async def check(self, client, key: str) -> CheckResult:
30
- """Real usage test - try to make a minimal chat completion request."""
31
- headers = self.build_headers(key)
32
- headers["Content-Type"] = "application/json"
33
- start = time.monotonic()
34
- try:
35
- resp = await client.post(
36
- f"{self.get_base_url()}/chat/completions",
37
- headers=headers,
38
- json={
39
- "model": "llama3.1-8b",
40
- "messages": [{"role": "user", "content": "hi"}],
41
- "max_tokens": 5
42
- }
43
- )
44
- latency = (time.monotonic() - start) * 1000
45
-
46
- if resp.status_code == 200:
47
- return CheckResult(True, 200, latency, None)
48
- elif resp.status_code in (401, 403):
49
- return CheckResult(False, resp.status_code, latency, "invalid key or forbidden")
50
- elif resp.status_code == 429:
51
- return CheckResult(False, 429, latency, "rate limited")
52
- else:
53
- try:
54
- data = resp.json()
55
- error_msg = data.get("error", {}).get("message", f"status {resp.status_code}")
56
- except:
57
- error_msg = f"status {resp.status_code}"
58
- return CheckResult(False, resp.status_code, latency, error_msg)
59
- except Exception as e:
60
- return CheckResult(False, None, (time.monotonic() - start) * 1000, str(e))
61
-
62
29
  async def test_token_limit(self, client, key: str, token_steps: list[int]) -> TestResult:
63
30
  headers = self.build_headers(key)
64
31
  last_success = None
@@ -8,7 +8,6 @@ class CSTCloudProvider(ProviderBase):
8
8
  name = "cstcloud"
9
9
  base_url = "https://uni-api.cstcloud.cn/v1"
10
10
  check_endpoint = "/models"
11
- check_model = "gpt-3.5-turbo"
12
11
 
13
12
  def build_headers(self, key: str) -> dict:
14
13
  return {"Authorization": f"Bearer {key}"}
@@ -25,33 +24,6 @@ class CSTCloudProvider(ProviderBase):
25
24
  except Exception:
26
25
  return []
27
26
 
28
- async def check(self, client, key: str) -> CheckResult:
29
- headers = self.build_headers(key)
30
- headers["Content-Type"] = "application/json"
31
- start = time.monotonic()
32
- try:
33
- resp = await client.post(
34
- f"{self.get_base_url()}/chat/completions",
35
- headers=headers,
36
- json={"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
37
- )
38
- latency = (time.monotonic() - start) * 1000
39
- if resp.status_code == 200:
40
- return CheckResult(True, 200, latency, None)
41
- elif resp.status_code in (401, 403):
42
- return CheckResult(False, resp.status_code, latency, "invalid key or forbidden")
43
- elif resp.status_code == 429:
44
- return CheckResult(False, 429, latency, "rate limited")
45
- else:
46
- try:
47
- data = resp.json()
48
- error_msg = data.get("error", {}).get("message", f"status {resp.status_code}")
49
- except:
50
- error_msg = f"status {resp.status_code}"
51
- return CheckResult(False, resp.status_code, latency, error_msg)
52
- except Exception as e:
53
- return CheckResult(False, None, (time.monotonic() - start) * 1000, str(e))
54
-
55
27
  async def test_token_limit(self, client, key: str, token_steps: list[int]) -> TestResult:
56
28
  headers = self.build_headers(key)
57
29
  last_success = None
@@ -7,7 +7,6 @@ class DashScopeProvider(ProviderBase):
7
7
  name = "dashscope"
8
8
  base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
9
9
  check_endpoint = "/models"
10
- check_model = "qwen-turbo"
11
10
 
12
11
  def build_headers(self, key: str) -> dict:
13
12
  return {"Authorization": f"Bearer {key}"}
@@ -24,33 +23,6 @@ class DashScopeProvider(ProviderBase):
24
23
  except Exception:
25
24
  return []
26
25
 
27
- async def check(self, client, key: str) -> CheckResult:
28
- headers = self.build_headers(key)
29
- headers["Content-Type"] = "application/json"
30
- start = time.monotonic()
31
- try:
32
- resp = await client.post(
33
- f"{self.get_base_url()}/chat/completions",
34
- headers=headers,
35
- json={"model": "qwen-turbo", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
36
- )
37
- latency = (time.monotonic() - start) * 1000
38
- if resp.status_code == 200:
39
- return CheckResult(True, 200, latency, None)
40
- elif resp.status_code in (401, 403):
41
- return CheckResult(False, resp.status_code, latency, "invalid key or forbidden")
42
- elif resp.status_code == 429:
43
- return CheckResult(False, 429, latency, "rate limited")
44
- else:
45
- try:
46
- data = resp.json()
47
- error_msg = data.get("error", {}).get("message", f"status {resp.status_code}")
48
- except:
49
- error_msg = f"status {resp.status_code}"
50
- return CheckResult(False, resp.status_code, latency, error_msg)
51
- except Exception as e:
52
- return CheckResult(False, None, (time.monotonic() - start) * 1000, str(e))
53
-
54
26
  async def test_token_limit(self, client, key: str, token_steps: list[int]) -> TestResult:
55
27
  headers = self.build_headers(key)
56
28
  last_success = None
@@ -8,7 +8,6 @@ class DashScopeCodingProvider(ProviderBase):
8
8
  name = "dashscope-coding"
9
9
  base_url = "https://coding-intl.dashscope.aliyuncs.com/compatible-mode/v1"
10
10
  check_endpoint = "/models"
11
- check_model = "qwen-coder-plus"
12
11
 
13
12
  def build_headers(self, key: str) -> dict:
14
13
  return {"Authorization": f"Bearer {key}"}
@@ -25,33 +24,6 @@ class DashScopeCodingProvider(ProviderBase):
25
24
  except Exception:
26
25
  return []
27
26
 
28
- async def check(self, client, key: str) -> CheckResult:
29
- headers = self.build_headers(key)
30
- headers["Content-Type"] = "application/json"
31
- start = time.monotonic()
32
- try:
33
- resp = await client.post(
34
- f"{self.get_base_url()}/chat/completions",
35
- headers=headers,
36
- json={"model": "qwen-coder-plus", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
37
- )
38
- latency = (time.monotonic() - start) * 1000
39
- if resp.status_code == 200:
40
- return CheckResult(True, 200, latency, None)
41
- elif resp.status_code in (401, 403):
42
- return CheckResult(False, resp.status_code, latency, "invalid key or forbidden")
43
- elif resp.status_code == 429:
44
- return CheckResult(False, 429, latency, "rate limited")
45
- else:
46
- try:
47
- data = resp.json()
48
- error_msg = data.get("error", {}).get("message", f"status {resp.status_code}")
49
- except:
50
- error_msg = f"status {resp.status_code}"
51
- return CheckResult(False, resp.status_code, latency, error_msg)
52
- except Exception as e:
53
- return CheckResult(False, None, (time.monotonic() - start) * 1000, str(e))
54
-
55
27
  async def test_token_limit(self, client, key: str, token_steps: list[int]) -> TestResult:
56
28
  headers = self.build_headers(key)
57
29
  last_success = None