api-key-manager 2.1.0__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.0 → api_key_manager-2.2.0}/PKG-INFO +38 -3
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/README.md +37 -2
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/PKG-INFO +38 -3
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/SOURCES.txt +11 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/__init__.py +3 -2
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/api_models.py +9 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/core.py +10 -4
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/detector.py +37 -9
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/__init__.py +9 -1
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/ai302.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/baichuan.py +0 -28
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/base.py +69 -31
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/cerebras.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/cstcloud.py +0 -28
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/dashscope.py +0 -28
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/dashscope_coding.py +0 -28
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/deepseek.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/dmxapi.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/doubao.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/fireworks.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/grok.py +0 -34
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/groq.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/huggingface.py +0 -17
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/hyperbolic.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/infini.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/infini_coding.py +96 -124
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/kimi.py +0 -28
- {api_key_manager-2.1.0 → 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.0 → api_key_manager-2.2.0}/key_manager/providers/mimo.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/mimo_plan.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/minimax.py +0 -28
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/minimax_plan.py +0 -28
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/mistral.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/models_registry.py +80 -19
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/modelscope.py +0 -14
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/nvidia.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/ocoolai.py +0 -33
- {api_key_manager-2.1.0 → 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.0 → api_key_manager-2.2.0}/key_manager/providers/openrouter.py +0 -28
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/perplexity.py +0 -34
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/poe.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/ppio.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/replicate.py +0 -17
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/siliconflow.py +0 -28
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/stepfun.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/tencent_hunyuan.py +0 -28
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/together.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/yi.py +0 -28
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/zai.py +0 -33
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/zhipu.py +0 -28
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/zhipu_coding.py +96 -124
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/proxy.py +0 -13
- api_key_manager-2.2.0/key_manager/py.typed +1 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/storage.py +10 -10
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/validator.py +1 -1
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/web.py +204 -157
- {api_key_manager-2.1.0 → 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.2.0/tests/test_bug_fixes.py +246 -0
- api_key_manager-2.2.0/tests/test_core.py +265 -0
- api_key_manager-2.2.0/tests/test_logger.py +273 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_provider_detection.py +10 -2
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_providers.py +1 -1
- api_key_manager-2.2.0/tests/test_proxy.py +122 -0
- api_key_manager-2.2.0/tests/test_tester.py +300 -0
- api_key_manager-2.2.0/tests/test_web_fixes.py +161 -0
- api_key_manager-2.1.0/key_manager/providers/longcat.py +0 -123
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/dependency_links.txt +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/entry_points.txt +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/requires.txt +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/top_level.txt +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/__main__.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/checker.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/cli.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/config.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/errors.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/i18n.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/logger.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/model_capabilities.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/parser.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/anthropic.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/cohere.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/google.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/ssrf.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/tester.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/url_override.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/webhook.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/setup.cfg +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_checker.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_detector.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_e2e.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_errors.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_i18n.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_openapi.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_parser.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_security.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_storage.py +0 -0
- {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_validator.py +0 -0
- {api_key_manager-2.1.0 → 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
|
### 国际
|
|
@@ -192,10 +197,11 @@ python web.py
|
|
|
192
197
|
### 安全防护
|
|
193
198
|
|
|
194
199
|
- **路径遍历防护** - 导入端点验证路径在允许目录内
|
|
195
|
-
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP
|
|
200
|
+
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP,已接入 `check/single` 和 `balance` 端点
|
|
196
201
|
- **时序安全认证** - 使用 `hmac.compare_digest()` 防止时序攻击
|
|
197
202
|
- **认证警告** - 未配置 API Key 时启动警告
|
|
198
203
|
- **密钥掩码** - API 响应中只返回 `key_masked`,不暴露完整密钥
|
|
204
|
+
- **Webhook 安全** - Webhook 端点使用正确的 API 方法,防止运行时错误
|
|
199
205
|
|
|
200
206
|
### API 认证
|
|
201
207
|
|
|
@@ -609,7 +615,7 @@ python -m pytest tests/ --cov=key_manager --cov-report=term-missing
|
|
|
609
615
|
| 密钥解析 | `test_parser.py` | 12 |
|
|
610
616
|
| 验证器 | `test_validator.py` | 5 |
|
|
611
617
|
| 检查器 | `test_checker.py` | 4 |
|
|
612
|
-
| 提供商合约 | `test_providers.py` |
|
|
618
|
+
| 提供商合约 | `test_providers.py` | 220 |
|
|
613
619
|
| 安全回归 | `test_security.py` | 12 |
|
|
614
620
|
| 加密存储 | `test_storage.py` | 26 |
|
|
615
621
|
| 错误系统 | `test_errors.py` | 28 |
|
|
@@ -617,7 +623,12 @@ python -m pytest tests/ --cov=key_manager --cov-report=term-missing
|
|
|
617
623
|
| 端到端 | `test_e2e.py` | 17 |
|
|
618
624
|
| Webhook | `test_webhook.py` | 35 |
|
|
619
625
|
| OpenAPI | `test_openapi.py` | 26 |
|
|
626
|
+
| 代理检测 | `test_proxy.py` | 19 |
|
|
627
|
+
| 日志系统 | `test_logger.py` | 21 |
|
|
628
|
+
| 能力测试 | `test_tester.py` | 11 |
|
|
629
|
+
| 核心门面 | `test_core.py` | 22 |
|
|
620
630
|
|
|
631
|
+
**总测试数**: 583+ | **覆盖率**: 88%
|
|
621
632
|
## SDK 使用
|
|
622
633
|
|
|
623
634
|
### Python SDK
|
|
@@ -704,6 +715,30 @@ const result = await client.checkSingleKey({ key: 'sk-xxx', provider: 'openai' }
|
|
|
704
715
|
|
|
705
716
|
模型列表从 Cherry Studio 同步,每日更新一次。新发布的模型可能需要等待同步后才能被检测到。
|
|
706
717
|
|
|
718
|
+
|
|
719
|
+
## 更新日志
|
|
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
|
+
|
|
731
|
+
### v2.1.2 (2026-06-11)
|
|
732
|
+
|
|
733
|
+
- **Bug 修复**: 修复 `KeyManager.detect_provider()` 调用异步函数未 await 的问题
|
|
734
|
+
- **测试扩展**: 提供商合约测试扩展至全部 44 个服务商
|
|
735
|
+
- **测试新增**: 新增 `test_proxy.py`、`test_logger.py`、`test_tester.py`、`test_core.py`
|
|
736
|
+
- **覆盖率提升**: 测试覆盖率从 74% 提升至 88%
|
|
737
|
+
|
|
738
|
+
### v2.1.1
|
|
739
|
+
|
|
740
|
+
- 初始发布版本
|
|
741
|
+
|
|
707
742
|
## 许可证
|
|
708
743
|
|
|
709
744
|
MIT License
|
|
@@ -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
|
### 国际
|
|
@@ -159,10 +164,11 @@ python web.py
|
|
|
159
164
|
### 安全防护
|
|
160
165
|
|
|
161
166
|
- **路径遍历防护** - 导入端点验证路径在允许目录内
|
|
162
|
-
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP
|
|
167
|
+
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP,已接入 `check/single` 和 `balance` 端点
|
|
163
168
|
- **时序安全认证** - 使用 `hmac.compare_digest()` 防止时序攻击
|
|
164
169
|
- **认证警告** - 未配置 API Key 时启动警告
|
|
165
170
|
- **密钥掩码** - API 响应中只返回 `key_masked`,不暴露完整密钥
|
|
171
|
+
- **Webhook 安全** - Webhook 端点使用正确的 API 方法,防止运行时错误
|
|
166
172
|
|
|
167
173
|
### API 认证
|
|
168
174
|
|
|
@@ -576,7 +582,7 @@ python -m pytest tests/ --cov=key_manager --cov-report=term-missing
|
|
|
576
582
|
| 密钥解析 | `test_parser.py` | 12 |
|
|
577
583
|
| 验证器 | `test_validator.py` | 5 |
|
|
578
584
|
| 检查器 | `test_checker.py` | 4 |
|
|
579
|
-
| 提供商合约 | `test_providers.py` |
|
|
585
|
+
| 提供商合约 | `test_providers.py` | 220 |
|
|
580
586
|
| 安全回归 | `test_security.py` | 12 |
|
|
581
587
|
| 加密存储 | `test_storage.py` | 26 |
|
|
582
588
|
| 错误系统 | `test_errors.py` | 28 |
|
|
@@ -584,7 +590,12 @@ python -m pytest tests/ --cov=key_manager --cov-report=term-missing
|
|
|
584
590
|
| 端到端 | `test_e2e.py` | 17 |
|
|
585
591
|
| Webhook | `test_webhook.py` | 35 |
|
|
586
592
|
| OpenAPI | `test_openapi.py` | 26 |
|
|
593
|
+
| 代理检测 | `test_proxy.py` | 19 |
|
|
594
|
+
| 日志系统 | `test_logger.py` | 21 |
|
|
595
|
+
| 能力测试 | `test_tester.py` | 11 |
|
|
596
|
+
| 核心门面 | `test_core.py` | 22 |
|
|
587
597
|
|
|
598
|
+
**总测试数**: 583+ | **覆盖率**: 88%
|
|
588
599
|
## SDK 使用
|
|
589
600
|
|
|
590
601
|
### Python SDK
|
|
@@ -671,6 +682,30 @@ const result = await client.checkSingleKey({ key: 'sk-xxx', provider: 'openai' }
|
|
|
671
682
|
|
|
672
683
|
模型列表从 Cherry Studio 同步,每日更新一次。新发布的模型可能需要等待同步后才能被检测到。
|
|
673
684
|
|
|
685
|
+
|
|
686
|
+
## 更新日志
|
|
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
|
+
|
|
698
|
+
### v2.1.2 (2026-06-11)
|
|
699
|
+
|
|
700
|
+
- **Bug 修复**: 修复 `KeyManager.detect_provider()` 调用异步函数未 await 的问题
|
|
701
|
+
- **测试扩展**: 提供商合约测试扩展至全部 44 个服务商
|
|
702
|
+
- **测试新增**: 新增 `test_proxy.py`、`test_logger.py`、`test_tester.py`、`test_core.py`
|
|
703
|
+
- **覆盖率提升**: 测试覆盖率从 74% 提升至 88%
|
|
704
|
+
|
|
705
|
+
### v2.1.1
|
|
706
|
+
|
|
707
|
+
- 初始发布版本
|
|
708
|
+
|
|
674
709
|
## 许可证
|
|
675
710
|
|
|
676
711
|
MIT License
|
|
@@ -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
|
### 国际
|
|
@@ -192,10 +197,11 @@ python web.py
|
|
|
192
197
|
### 安全防护
|
|
193
198
|
|
|
194
199
|
- **路径遍历防护** - 导入端点验证路径在允许目录内
|
|
195
|
-
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP
|
|
200
|
+
- **SSRF 防护** - `custom_base_url` 验证域名白名单,阻止私有 IP,已接入 `check/single` 和 `balance` 端点
|
|
196
201
|
- **时序安全认证** - 使用 `hmac.compare_digest()` 防止时序攻击
|
|
197
202
|
- **认证警告** - 未配置 API Key 时启动警告
|
|
198
203
|
- **密钥掩码** - API 响应中只返回 `key_masked`,不暴露完整密钥
|
|
204
|
+
- **Webhook 安全** - Webhook 端点使用正确的 API 方法,防止运行时错误
|
|
199
205
|
|
|
200
206
|
### API 认证
|
|
201
207
|
|
|
@@ -609,7 +615,7 @@ python -m pytest tests/ --cov=key_manager --cov-report=term-missing
|
|
|
609
615
|
| 密钥解析 | `test_parser.py` | 12 |
|
|
610
616
|
| 验证器 | `test_validator.py` | 5 |
|
|
611
617
|
| 检查器 | `test_checker.py` | 4 |
|
|
612
|
-
| 提供商合约 | `test_providers.py` |
|
|
618
|
+
| 提供商合约 | `test_providers.py` | 220 |
|
|
613
619
|
| 安全回归 | `test_security.py` | 12 |
|
|
614
620
|
| 加密存储 | `test_storage.py` | 26 |
|
|
615
621
|
| 错误系统 | `test_errors.py` | 28 |
|
|
@@ -617,7 +623,12 @@ python -m pytest tests/ --cov=key_manager --cov-report=term-missing
|
|
|
617
623
|
| 端到端 | `test_e2e.py` | 17 |
|
|
618
624
|
| Webhook | `test_webhook.py` | 35 |
|
|
619
625
|
| OpenAPI | `test_openapi.py` | 26 |
|
|
626
|
+
| 代理检测 | `test_proxy.py` | 19 |
|
|
627
|
+
| 日志系统 | `test_logger.py` | 21 |
|
|
628
|
+
| 能力测试 | `test_tester.py` | 11 |
|
|
629
|
+
| 核心门面 | `test_core.py` | 22 |
|
|
620
630
|
|
|
631
|
+
**总测试数**: 583+ | **覆盖率**: 88%
|
|
621
632
|
## SDK 使用
|
|
622
633
|
|
|
623
634
|
### Python SDK
|
|
@@ -704,6 +715,30 @@ const result = await client.checkSingleKey({ key: 'sk-xxx', provider: 'openai' }
|
|
|
704
715
|
|
|
705
716
|
模型列表从 Cherry Studio 同步,每日更新一次。新发布的模型可能需要等待同步后才能被检测到。
|
|
706
717
|
|
|
718
|
+
|
|
719
|
+
## 更新日志
|
|
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
|
+
|
|
731
|
+
### v2.1.2 (2026-06-11)
|
|
732
|
+
|
|
733
|
+
- **Bug 修复**: 修复 `KeyManager.detect_provider()` 调用异步函数未 await 的问题
|
|
734
|
+
- **测试扩展**: 提供商合约测试扩展至全部 44 个服务商
|
|
735
|
+
- **测试新增**: 新增 `test_proxy.py`、`test_logger.py`、`test_tester.py`、`test_core.py`
|
|
736
|
+
- **覆盖率提升**: 测试覆盖率从 74% 提升至 88%
|
|
737
|
+
|
|
738
|
+
### v2.1.1
|
|
739
|
+
|
|
740
|
+
- 初始发布版本
|
|
741
|
+
|
|
707
742
|
## 许可证
|
|
708
743
|
|
|
709
744
|
MIT License
|
|
@@ -20,6 +20,7 @@ key_manager/logger.py
|
|
|
20
20
|
key_manager/model_capabilities.py
|
|
21
21
|
key_manager/parser.py
|
|
22
22
|
key_manager/proxy.py
|
|
23
|
+
key_manager/py.typed
|
|
23
24
|
key_manager/ssrf.py
|
|
24
25
|
key_manager/storage.py
|
|
25
26
|
key_manager/tester.py
|
|
@@ -61,6 +62,8 @@ key_manager/providers/modelscope.py
|
|
|
61
62
|
key_manager/providers/nvidia.py
|
|
62
63
|
key_manager/providers/ocoolai.py
|
|
63
64
|
key_manager/providers/openai.py
|
|
65
|
+
key_manager/providers/opencode.py
|
|
66
|
+
key_manager/providers/opencode_zen.py
|
|
64
67
|
key_manager/providers/openrouter.py
|
|
65
68
|
key_manager/providers/perplexity.py
|
|
66
69
|
key_manager/providers/poe.py
|
|
@@ -74,16 +77,24 @@ key_manager/providers/yi.py
|
|
|
74
77
|
key_manager/providers/zai.py
|
|
75
78
|
key_manager/providers/zhipu.py
|
|
76
79
|
key_manager/providers/zhipu_coding.py
|
|
80
|
+
tests/test_api_endpoints.py
|
|
81
|
+
tests/test_base_check.py
|
|
82
|
+
tests/test_bug_fixes.py
|
|
77
83
|
tests/test_checker.py
|
|
84
|
+
tests/test_core.py
|
|
78
85
|
tests/test_detector.py
|
|
79
86
|
tests/test_e2e.py
|
|
80
87
|
tests/test_errors.py
|
|
81
88
|
tests/test_i18n.py
|
|
89
|
+
tests/test_logger.py
|
|
82
90
|
tests/test_openapi.py
|
|
83
91
|
tests/test_parser.py
|
|
84
92
|
tests/test_provider_detection.py
|
|
85
93
|
tests/test_providers.py
|
|
94
|
+
tests/test_proxy.py
|
|
86
95
|
tests/test_security.py
|
|
87
96
|
tests/test_storage.py
|
|
97
|
+
tests/test_tester.py
|
|
88
98
|
tests/test_validator.py
|
|
99
|
+
tests/test_web_fixes.py
|
|
89
100
|
tests/test_webhook.py
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Key Manager - Batch API key management for 44+ AI providers."""
|
|
1
|
+
"""Key Manager - Batch API key management for 44+ AI providers."""
|
|
2
2
|
from key_manager.core import KeyManager
|
|
3
3
|
from key_manager.providers import PROVIDERS, get_display_name
|
|
4
4
|
from key_manager.errors import KeyManagerError, ErrorCode
|
|
@@ -13,4 +13,5 @@ __all__ = [
|
|
|
13
13
|
"KeyStore",
|
|
14
14
|
]
|
|
15
15
|
|
|
16
|
-
__version__ = "2.
|
|
16
|
+
__version__ = "2.2.0"
|
|
17
|
+
|
|
@@ -42,6 +42,7 @@ __all__ = [
|
|
|
42
42
|
"OperationEntry",
|
|
43
43
|
"OperationsResponse",
|
|
44
44
|
"ProgressResponse",
|
|
45
|
+
"ProxyResponse",
|
|
45
46
|
]
|
|
46
47
|
|
|
47
48
|
|
|
@@ -356,3 +357,11 @@ class ProgressResponse(BaseModel):
|
|
|
356
357
|
total: int = Field(0, description="Total items to process")
|
|
357
358
|
status: str = Field("", description="Task status: loading / done / error")
|
|
358
359
|
results: dict[str, Any] | None = Field(None, description="Final results when complete")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ── Proxy ─────────────────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class ProxyResponse(BaseModel):
|
|
366
|
+
proxy: str | None = Field(None, description="Detected proxy URL")
|
|
367
|
+
source: str = Field("", description="Proxy source: config / env / auto / none")
|
|
@@ -62,10 +62,16 @@ class KeyManager:
|
|
|
62
62
|
"""Save keys to encrypted storage."""
|
|
63
63
|
self._store.save(data)
|
|
64
64
|
|
|
65
|
-
def detect_provider(self, key: str) -> list[str]:
|
|
65
|
+
async def detect_provider(self, key: str) -> list[str]:
|
|
66
66
|
"""Auto-detect provider from key prefix."""
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
import httpx
|
|
68
|
+
from key_manager.detector import detect_provider as _detect_provider
|
|
69
|
+
|
|
70
|
+
timeout = self.config.get("check", {}).get("timeout_seconds", 30)
|
|
71
|
+
proxy = self.config.get("proxy") or None
|
|
72
|
+
async with httpx.AsyncClient(timeout=timeout, proxy=proxy) as client:
|
|
73
|
+
result = await _detect_provider(client, key)
|
|
74
|
+
return [result] if result else []
|
|
69
75
|
def list_keys(
|
|
70
76
|
self,
|
|
71
77
|
provider: str | None = None,
|
|
@@ -139,7 +145,7 @@ class KeyManager:
|
|
|
139
145
|
from key_manager.providers import PROVIDERS
|
|
140
146
|
|
|
141
147
|
if not provider:
|
|
142
|
-
providers = self.detect_provider(key)
|
|
148
|
+
providers = await self.detect_provider(key)
|
|
143
149
|
if not providers:
|
|
144
150
|
return {"valid": False, "error": "Could not detect provider"}
|
|
145
151
|
provider = providers[0]
|
|
@@ -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
|