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.
Files changed (103) hide show
  1. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/PKG-INFO +38 -3
  2. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/README.md +37 -2
  3. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/PKG-INFO +38 -3
  4. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/SOURCES.txt +11 -0
  5. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/__init__.py +3 -2
  6. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/api_models.py +9 -0
  7. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/core.py +10 -4
  8. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/detector.py +37 -9
  9. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/__init__.py +9 -1
  10. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/ai302.py +0 -33
  11. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/baichuan.py +0 -28
  12. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/base.py +69 -31
  13. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/cerebras.py +0 -33
  14. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/cstcloud.py +0 -28
  15. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/dashscope.py +0 -28
  16. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/dashscope_coding.py +0 -28
  17. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/deepseek.py +0 -33
  18. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/dmxapi.py +0 -33
  19. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/doubao.py +0 -33
  20. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/fireworks.py +0 -33
  21. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/grok.py +0 -34
  22. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/groq.py +0 -33
  23. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/huggingface.py +0 -17
  24. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/hyperbolic.py +0 -33
  25. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/infini.py +0 -33
  26. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/infini_coding.py +96 -124
  27. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/kimi.py +0 -28
  28. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/kimi_coding.py +96 -124
  29. api_key_manager-2.2.0/key_manager/providers/longcat.py +76 -0
  30. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/mimo.py +0 -33
  31. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/mimo_plan.py +0 -33
  32. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/minimax.py +0 -28
  33. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/minimax_plan.py +0 -28
  34. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/mistral.py +0 -33
  35. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/models_registry.py +80 -19
  36. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/modelscope.py +0 -14
  37. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/nvidia.py +0 -33
  38. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/ocoolai.py +0 -33
  39. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/openai.py +0 -39
  40. api_key_manager-2.2.0/key_manager/providers/opencode.py +89 -0
  41. api_key_manager-2.2.0/key_manager/providers/opencode_zen.py +89 -0
  42. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/openrouter.py +0 -28
  43. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/perplexity.py +0 -34
  44. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/poe.py +0 -33
  45. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/ppio.py +0 -33
  46. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/replicate.py +0 -17
  47. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/siliconflow.py +0 -28
  48. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/stepfun.py +0 -33
  49. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/tencent_hunyuan.py +0 -28
  50. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/together.py +0 -33
  51. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/yi.py +0 -28
  52. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/zai.py +0 -33
  53. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/zhipu.py +0 -28
  54. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/zhipu_coding.py +96 -124
  55. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/proxy.py +0 -13
  56. api_key_manager-2.2.0/key_manager/py.typed +1 -0
  57. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/storage.py +10 -10
  58. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/validator.py +1 -1
  59. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/web.py +204 -157
  60. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/pyproject.toml +1 -1
  61. api_key_manager-2.2.0/tests/test_api_endpoints.py +500 -0
  62. api_key_manager-2.2.0/tests/test_base_check.py +255 -0
  63. api_key_manager-2.2.0/tests/test_bug_fixes.py +246 -0
  64. api_key_manager-2.2.0/tests/test_core.py +265 -0
  65. api_key_manager-2.2.0/tests/test_logger.py +273 -0
  66. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_provider_detection.py +10 -2
  67. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_providers.py +1 -1
  68. api_key_manager-2.2.0/tests/test_proxy.py +122 -0
  69. api_key_manager-2.2.0/tests/test_tester.py +300 -0
  70. api_key_manager-2.2.0/tests/test_web_fixes.py +161 -0
  71. api_key_manager-2.1.0/key_manager/providers/longcat.py +0 -123
  72. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/dependency_links.txt +0 -0
  73. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/entry_points.txt +0 -0
  74. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/requires.txt +0 -0
  75. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/api_key_manager.egg-info/top_level.txt +0 -0
  76. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/__main__.py +0 -0
  77. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/checker.py +0 -0
  78. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/cli.py +0 -0
  79. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/config.py +0 -0
  80. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/errors.py +0 -0
  81. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/i18n.py +0 -0
  82. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/logger.py +0 -0
  83. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/model_capabilities.py +0 -0
  84. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/parser.py +0 -0
  85. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/anthropic.py +0 -0
  86. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/cohere.py +0 -0
  87. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/providers/google.py +0 -0
  88. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/ssrf.py +0 -0
  89. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/tester.py +0 -0
  90. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/url_override.py +0 -0
  91. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/key_manager/webhook.py +0 -0
  92. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/setup.cfg +0 -0
  93. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_checker.py +0 -0
  94. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_detector.py +0 -0
  95. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_e2e.py +0 -0
  96. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_errors.py +0 -0
  97. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_i18n.py +0 -0
  98. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_openapi.py +0 -0
  99. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_parser.py +0 -0
  100. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_security.py +0 -0
  101. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_storage.py +0 -0
  102. {api_key_manager-2.1.0 → api_key_manager-2.2.0}/tests/test_validator.py +0 -0
  103. {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.1.0
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
  ### 国际
@@ -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` | 30 |
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
+ ![系统架构流程图](docs/images/flowchart.png)
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` | 30 |
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.1.0
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
  ### 国际
@@ -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` | 30 |
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.1.0"
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
- return detect_provider(key)
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 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