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.
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/PKG-INFO +16 -1
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/README.md +15 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/PKG-INFO +16 -1
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/SOURCES.txt +4 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/__init__.py +1 -1
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/detector.py +37 -9
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/__init__.py +9 -1
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/ai302.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/baichuan.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/base.py +69 -31
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/cerebras.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/cstcloud.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/dashscope.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/dashscope_coding.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/deepseek.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/dmxapi.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/doubao.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/fireworks.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/grok.py +0 -34
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/groq.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/huggingface.py +0 -17
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/hyperbolic.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/infini.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/infini_coding.py +96 -124
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/kimi.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/kimi_coding.py +96 -124
- api_key_manager-2.2.0/key_manager/providers/longcat.py +76 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/mimo.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/mimo_plan.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/minimax.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/minimax_plan.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/mistral.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/models_registry.py +80 -19
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/modelscope.py +0 -14
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/nvidia.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/ocoolai.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/openai.py +0 -39
- api_key_manager-2.2.0/key_manager/providers/opencode.py +89 -0
- api_key_manager-2.2.0/key_manager/providers/opencode_zen.py +89 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/openrouter.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/perplexity.py +0 -34
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/poe.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/ppio.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/replicate.py +0 -17
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/siliconflow.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/stepfun.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/tencent_hunyuan.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/together.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/yi.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/zai.py +0 -33
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/zhipu.py +0 -28
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/zhipu_coding.py +96 -124
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/web.py +11 -9
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/pyproject.toml +1 -1
- api_key_manager-2.2.0/tests/test_api_endpoints.py +500 -0
- api_key_manager-2.2.0/tests/test_base_check.py +255 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_provider_detection.py +10 -2
- api_key_manager-2.1.2/key_manager/providers/longcat.py +0 -123
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/dependency_links.txt +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/entry_points.txt +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/requires.txt +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/api_key_manager.egg-info/top_level.txt +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/__main__.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/api_models.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/checker.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/cli.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/config.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/core.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/errors.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/i18n.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/logger.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/model_capabilities.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/parser.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/anthropic.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/cohere.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/providers/google.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/proxy.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/py.typed +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/ssrf.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/storage.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/tester.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/url_override.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/validator.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/key_manager/webhook.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/setup.cfg +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_bug_fixes.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_checker.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_core.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_detector.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_e2e.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_errors.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_i18n.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_logger.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_openapi.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_parser.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_providers.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_proxy.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_security.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_storage.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_tester.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_validator.py +0 -0
- {api_key_manager-2.1.2 → api_key_manager-2.2.0}/tests/test_web_fixes.py +0 -0
- {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.
|
|
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
|
+

|
|
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
|
+

|
|
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.
|
|
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
|
+

|
|
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
|
|
@@ -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
|
|
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,
|
|
230
|
-
models = PROVIDER_MODELS.get(name, [])
|
|
254
|
+
for name, models in model_results:
|
|
231
255
|
if not models:
|
|
232
|
-
|
|
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
|
-
|
|
274
|
+
chat_url,
|
|
247
275
|
headers=headers,
|
|
248
276
|
json={"model": model, "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
|
|
249
277
|
),
|
|
250
|
-
timeout=
|
|
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
|
-
"""
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
#
|
|
151
|
-
|
|
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
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
189
|
-
last_status = None
|
|
209
|
+
return CheckResult(False, None, (time.monotonic() - start) * 1000, str(e))
|
|
190
210
|
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
|
|
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
|