api-key-manager 2.1.0__py3-none-any.whl

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 (73) hide show
  1. api_key_manager-2.1.0.dist-info/METADATA +709 -0
  2. api_key_manager-2.1.0.dist-info/RECORD +73 -0
  3. api_key_manager-2.1.0.dist-info/WHEEL +5 -0
  4. api_key_manager-2.1.0.dist-info/entry_points.txt +2 -0
  5. api_key_manager-2.1.0.dist-info/top_level.txt +1 -0
  6. key_manager/__init__.py +16 -0
  7. key_manager/__main__.py +5 -0
  8. key_manager/api_models.py +358 -0
  9. key_manager/checker.py +51 -0
  10. key_manager/cli.py +270 -0
  11. key_manager/config.py +61 -0
  12. key_manager/core.py +205 -0
  13. key_manager/detector.py +335 -0
  14. key_manager/errors.py +179 -0
  15. key_manager/i18n.py +142 -0
  16. key_manager/logger.py +207 -0
  17. key_manager/model_capabilities.py +412 -0
  18. key_manager/parser.py +153 -0
  19. key_manager/providers/__init__.py +283 -0
  20. key_manager/providers/ai302.py +109 -0
  21. key_manager/providers/anthropic.py +109 -0
  22. key_manager/providers/baichuan.py +97 -0
  23. key_manager/providers/base.py +312 -0
  24. key_manager/providers/cerebras.py +109 -0
  25. key_manager/providers/cohere.py +90 -0
  26. key_manager/providers/cstcloud.py +122 -0
  27. key_manager/providers/dashscope.py +120 -0
  28. key_manager/providers/dashscope_coding.py +122 -0
  29. key_manager/providers/deepseek.py +166 -0
  30. key_manager/providers/dmxapi.py +109 -0
  31. key_manager/providers/doubao.py +109 -0
  32. key_manager/providers/fireworks.py +109 -0
  33. key_manager/providers/google.py +99 -0
  34. key_manager/providers/grok.py +109 -0
  35. key_manager/providers/groq.py +109 -0
  36. key_manager/providers/huggingface.py +54 -0
  37. key_manager/providers/hyperbolic.py +109 -0
  38. key_manager/providers/infini.py +135 -0
  39. key_manager/providers/infini_coding.py +124 -0
  40. key_manager/providers/kimi.py +121 -0
  41. key_manager/providers/kimi_coding.py +124 -0
  42. key_manager/providers/longcat.py +123 -0
  43. key_manager/providers/mimo.py +109 -0
  44. key_manager/providers/mimo_plan.py +140 -0
  45. key_manager/providers/minimax.py +97 -0
  46. key_manager/providers/minimax_plan.py +122 -0
  47. key_manager/providers/mistral.py +109 -0
  48. key_manager/providers/models_registry.py +2901 -0
  49. key_manager/providers/modelscope.py +134 -0
  50. key_manager/providers/nvidia.py +109 -0
  51. key_manager/providers/ocoolai.py +109 -0
  52. key_manager/providers/openai.py +140 -0
  53. key_manager/providers/openrouter.py +119 -0
  54. key_manager/providers/perplexity.py +109 -0
  55. key_manager/providers/poe.py +109 -0
  56. key_manager/providers/ppio.py +109 -0
  57. key_manager/providers/replicate.py +54 -0
  58. key_manager/providers/siliconflow.py +121 -0
  59. key_manager/providers/stepfun.py +132 -0
  60. key_manager/providers/tencent_hunyuan.py +122 -0
  61. key_manager/providers/together.py +134 -0
  62. key_manager/providers/yi.py +97 -0
  63. key_manager/providers/zai.py +109 -0
  64. key_manager/providers/zhipu.py +127 -0
  65. key_manager/providers/zhipu_coding.py +124 -0
  66. key_manager/proxy.py +70 -0
  67. key_manager/ssrf.py +68 -0
  68. key_manager/storage.py +134 -0
  69. key_manager/tester.py +137 -0
  70. key_manager/url_override.py +5 -0
  71. key_manager/validator.py +185 -0
  72. key_manager/web.py +1512 -0
  73. key_manager/webhook.py +257 -0
key_manager/parser.py ADDED
@@ -0,0 +1,153 @@
1
+ import json
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+
7
+ def load_keys_file(path: str) -> dict:
8
+ keys_path = Path(path)
9
+ if keys_path.exists():
10
+ with open(keys_path, "r", encoding="utf-8") as f:
11
+ return json.load(f)
12
+ return {
13
+ "version": "1.0",
14
+ "updated_at": datetime.utcnow().isoformat() + "Z",
15
+ "imports": [],
16
+ "keys": {}
17
+ }
18
+
19
+
20
+ def save_keys_file(data: dict, path: str):
21
+ data["updated_at"] = datetime.utcnow().isoformat() + "Z"
22
+ with open(path, "w", encoding="utf-8") as f:
23
+ json.dump(data, f, indent=2, ensure_ascii=False)
24
+
25
+
26
+ def mask_key(key: str) -> str:
27
+ if len(key) <= 10:
28
+ return key
29
+ return f"{key[:6]}...{key[-4:]}"
30
+
31
+
32
+ def extract_batch_from_filename(filename: str) -> str:
33
+ import re
34
+ match = re.search(r"(\d{4}-\d{2}-\d{2})", filename)
35
+ if match:
36
+ return match.group(1)
37
+ return datetime.now().strftime("%Y-%m-%d")
38
+
39
+
40
+ def validate_import_path(path: str, allowed_dirs: list[str]) -> Path:
41
+ """Validate path is within allowed directories. Raises ValidationError if not."""
42
+ from key_manager.errors import ErrorCode, ValidationError
43
+ resolved = Path(path).resolve()
44
+ for allowed in allowed_dirs:
45
+ allowed_resolved = Path(allowed).resolve()
46
+ try:
47
+ resolved.relative_to(allowed_resolved)
48
+ return resolved
49
+ except ValueError:
50
+ continue
51
+ raise ValidationError(
52
+ code=ErrorCode.VALIDATION_FILE_NOT_FOUND,
53
+ message="Path outside allowed directories"
54
+ )
55
+
56
+
57
+ def import_keys(file_path: Optional[str] = None,
58
+ directory: Optional[str] = None,
59
+ batch: Optional[str] = None,
60
+ keys_file: str = "./data/keys.json") -> tuple[int, int, list[str]]:
61
+ errors = []
62
+ new_keys = 0
63
+ duplicates = 0
64
+
65
+ data = load_keys_file(keys_file)
66
+ files_to_process = []
67
+
68
+ if file_path:
69
+ p = Path(file_path)
70
+ if p.exists() and p.suffix == ".json":
71
+ files_to_process.append(p)
72
+ else:
73
+ errors.append(f"File not found or not JSON: {file_path}")
74
+ return new_keys, duplicates, errors
75
+ elif directory:
76
+ d = Path(directory)
77
+ if d.exists():
78
+ files_to_process.extend(d.glob("*.json"))
79
+ else:
80
+ errors.append(f"Directory not found: {directory}")
81
+ return new_keys, duplicates, errors
82
+ else:
83
+ errors.append("No file or directory specified")
84
+ return new_keys, duplicates, errors
85
+
86
+ for fp in files_to_process:
87
+ try:
88
+ with open(fp, "r", encoding="utf-8") as f:
89
+ items = json.load(f)
90
+
91
+ if not isinstance(items, list):
92
+ errors.append(f"{fp.name}: not a JSON array")
93
+ continue
94
+
95
+ file_batch = batch or extract_batch_from_filename(fp.name)
96
+ import_record = {
97
+ "file": fp.name,
98
+ "batch": file_batch,
99
+ "imported_at": datetime.utcnow().isoformat() + "Z",
100
+ "key_count": len(items),
101
+ "new_keys": 0,
102
+ "duplicates": 0
103
+ }
104
+
105
+ for item in items:
106
+ if not isinstance(item, dict) or "key" not in item:
107
+ continue
108
+
109
+ key = item["key"]
110
+ if key in data["keys"]:
111
+ data["keys"][key]["sources"].append({
112
+ "file": item.get("file_path", ""),
113
+ "batch": file_batch,
114
+ "imported_at": datetime.utcnow().isoformat() + "Z",
115
+ "original_path": item.get("file_path", ""),
116
+ "repo": item.get("repo_name", ""),
117
+ "repo_url": item.get("repo_url", ""),
118
+ "found_at": item.get("found_at", "")
119
+ })
120
+ duplicates += 1
121
+ import_record["duplicates"] += 1
122
+ else:
123
+ data["keys"][key] = {
124
+ "key_masked": mask_key(key),
125
+ "provider": item.get("provider", "unknown"),
126
+ "provider_detected": None,
127
+ "status": "unknown",
128
+ "sources": [{
129
+ "file": item.get("file_path", ""),
130
+ "batch": file_batch,
131
+ "imported_at": datetime.utcnow().isoformat() + "Z",
132
+ "original_path": item.get("file_path", ""),
133
+ "repo": item.get("repo_name", ""),
134
+ "repo_url": item.get("repo_url", ""),
135
+ "found_at": item.get("found_at", "")
136
+ }],
137
+ "checks": [],
138
+ "tests": {},
139
+ "first_seen": datetime.utcnow().isoformat() + "Z",
140
+ "last_checked": None,
141
+ "last_tested": None,
142
+ "created_at": datetime.utcnow().isoformat() + "Z"
143
+ }
144
+ new_keys += 1
145
+ import_record["new_keys"] += 1
146
+
147
+ data["imports"].append(import_record)
148
+ save_keys_file(data, keys_file)
149
+
150
+ except Exception as e:
151
+ errors.append(f"{fp.name}: {str(e)}")
152
+
153
+ return new_keys, duplicates, errors
@@ -0,0 +1,283 @@
1
+ from .base import ProviderBase, CheckResult, TestResult
2
+ from .openai import OpenAIProvider
3
+ from .anthropic import AnthropicProvider
4
+ from .google import GoogleProvider
5
+ from .grok import GrokProvider
6
+ from .deepseek import DeepSeekProvider
7
+ from .groq import GroqProvider
8
+ from .perplexity import PerplexityProvider
9
+ from .together import TogetherProvider
10
+ from .mistral import MistralProvider
11
+ from .cohere import CohereProvider
12
+ from .replicate import ReplicateProvider
13
+ from .huggingface import HuggingFaceProvider
14
+ from .fireworks import FireworksProvider
15
+ from .openrouter import OpenRouterProvider
16
+ from .dashscope import DashScopeProvider
17
+ from .modelscope import ModelScopeProvider
18
+ from .zhipu import ZhipuProvider
19
+ from .kimi import KimiProvider
20
+ from .minimax import MiniMaxProvider
21
+ from .minimax_plan import MiniMaxTokenPlanProvider
22
+ from .siliconflow import SiliconFlowProvider
23
+ from .baichuan import BaichuanProvider
24
+ from .yi import YiProvider
25
+ from .cerebras import CerebrasProvider
26
+ from .nvidia import NvidiaProvider
27
+ from .hyperbolic import HyperbolicProvider
28
+ from .poe import PoeProvider
29
+ from .longcat import LongCatProvider
30
+ from .mimo import MiMoProvider
31
+ from .mimo_plan import MiMoPlanProvider
32
+ from .stepfun import StepFunProvider
33
+ from .doubao import DoubaoProvider
34
+ from .infini import InfiniProvider
35
+ from .zai import ZAIProvider
36
+ from .ai302 import AI302Provider
37
+ from .ppio import PPIOProvider
38
+ from .dmxapi import DMXAPIProvider
39
+ from .ocoolai import OCoolAIProvider
40
+ from .dashscope_coding import DashScopeCodingProvider
41
+ from .tencent_hunyuan import TencentHunyuanProvider
42
+ from .cstcloud import CSTCloudProvider
43
+ from .zhipu_coding import ZhipuCodingProvider
44
+ from .kimi_coding import KimiCodingProvider
45
+ from .infini_coding import InfiniCodingProvider
46
+
47
+ # Provider registry
48
+ PROVIDERS: dict[str, ProviderBase] = {
49
+ "openai": OpenAIProvider(),
50
+ "anthropic": AnthropicProvider(),
51
+ "google": GoogleProvider(),
52
+ "grok": GrokProvider(),
53
+ "deepseek": DeepSeekProvider(),
54
+ "groq": GroqProvider(),
55
+ "perplexity": PerplexityProvider(),
56
+ "together": TogetherProvider(),
57
+ "mistral": MistralProvider(),
58
+ "cohere": CohereProvider(),
59
+ "replicate": ReplicateProvider(),
60
+ "huggingface": HuggingFaceProvider(),
61
+ "fireworks": FireworksProvider(),
62
+ "openrouter": OpenRouterProvider(),
63
+ "dashscope": DashScopeProvider(),
64
+ "modelscope": ModelScopeProvider(),
65
+ "zhipu": ZhipuProvider(),
66
+ "kimi": KimiProvider(),
67
+ "minimax": MiniMaxProvider(),
68
+ "minimax-plan": MiniMaxTokenPlanProvider(),
69
+ "siliconflow": SiliconFlowProvider(),
70
+ "baichuan": BaichuanProvider(),
71
+ "yi": YiProvider(),
72
+ "cerebras": CerebrasProvider(),
73
+ "nvidia": NvidiaProvider(),
74
+ "hyperbolic": HyperbolicProvider(),
75
+ "poe": PoeProvider(),
76
+ "longcat": LongCatProvider(),
77
+ "mimo": MiMoProvider(),
78
+ "mimo-plan": MiMoPlanProvider(),
79
+ "stepfun": StepFunProvider(),
80
+ "doubao": DoubaoProvider(),
81
+ "infini": InfiniProvider(),
82
+ "zai": ZAIProvider(),
83
+ "ai302": AI302Provider(),
84
+ "ppio": PPIOProvider(),
85
+ "dmxapi": DMXAPIProvider(),
86
+ "ocoolai": OCoolAIProvider(),
87
+ "dashscope-coding": DashScopeCodingProvider(),
88
+ "tencent-hunyuan": TencentHunyuanProvider(),
89
+ "cstcloud": CSTCloudProvider(),
90
+ "zhipu-coding": ZhipuCodingProvider(),
91
+ "kimi-coding": KimiCodingProvider(),
92
+ "infini-coding": InfiniCodingProvider(),
93
+ }
94
+
95
+ # Key prefix to provider mapping (for auto-detection)
96
+ # Key prefix to provider mapping (for auto-detection)
97
+ # Ordered by specificity (longer prefixes first)
98
+ KEY_PREFIX_MAP: dict[str, list[str]] = {
99
+ # AI Providers - unique prefixes
100
+ "sk-ant-api03-": ["anthropic"],
101
+ "sk-or-v1-": ["openrouter"],
102
+ "sk-proj-": ["openai"],
103
+ "sk-sp-": ["dashscope"],
104
+ "ms-": ["modelscope"],
105
+ "AIza": ["google"],
106
+ "xai-": ["grok"],
107
+ "hf_": ["huggingface"],
108
+ "r8_": ["replicate"],
109
+ "pplx-": ["perplexity"],
110
+ "gsk_": ["groq"],
111
+ "fw_": ["fireworks"],
112
+ "poe-": ["poe"],
113
+ "AKID": ["cstcloud"],
114
+ # Generic sk- prefix (must be last - shared by multiple providers)
115
+ # 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"],
117
+ # MiniMax Token Plan keys
118
+ "sk-cp-": ["minimax-plan", "infini-coding"],
119
+ # Kimi Coding Plan keys
120
+ "sk-kimi-": ["kimi-coding"],
121
+ # MiMo Token Plan keys
122
+ "tp-": ["mimo-plan"],
123
+ }
124
+
125
+ # Display names for UI (maps internal provider name → human-readable name)
126
+ DISPLAY_NAMES: dict[str, str] = {
127
+ "openai": "OpenAI",
128
+ "anthropic": "Anthropic",
129
+ "google": "Google Gemini",
130
+ "deepseek": "DeepSeek",
131
+ "groq": "Groq",
132
+ "grok": "Grok (xAI)",
133
+ "perplexity": "Perplexity",
134
+ "together": "Together AI",
135
+ "mistral": "Mistral",
136
+ "cohere": "Cohere",
137
+ "replicate": "Replicate",
138
+ "huggingface": "Hugging Face",
139
+ "fireworks": "Fireworks",
140
+ "openrouter": "OpenRouter",
141
+ "dashscope": "阿里百炼",
142
+ "dashscope-coding": "阿里百炼编程",
143
+ "modelscope": "魔搭 ModelScope",
144
+ "zhipu": "智谱 GLM",
145
+ "kimi": "Kimi (月之暗面)",
146
+ "minimax": "MiniMax",
147
+ "minimax-plan": "MiniMax 计划版",
148
+ "siliconflow": "硅基流动",
149
+ "baichuan": "百川",
150
+ "yi": "零一万物",
151
+ "cerebras": "Cerebras",
152
+ "nvidia": "NVIDIA",
153
+ "hyperbolic": "Hyperbolic",
154
+ "poe": "Poe",
155
+ "longcat": "LongCat",
156
+ "mimo": "MiMo",
157
+ "mimo-plan": "MiMo 计划版",
158
+ "stepfun": "阶跃星辰",
159
+ "doubao": "豆包 (字节)",
160
+ "infini": "无问芯穹",
161
+ "zai": "ZAI",
162
+ "ai302": "AI302",
163
+ "ppio": "PPIO",
164
+ "dmxapi": "DMXAPI",
165
+ "ocoolai": "OCoolAI",
166
+ "tencent-hunyuan": "腾讯混元",
167
+ "zhipu-coding": "智谱 GLM 编程版",
168
+ "kimi-coding": "Kimi 编程版",
169
+ "infini-coding": "无问芯穹 编程版",
170
+ "cstcloud": "中算云",
171
+ }
172
+
173
+
174
+ def get_display_name(provider_name: str) -> str:
175
+ """Get human-readable display name for a provider."""
176
+ return DISPLAY_NAMES.get(provider_name, provider_name)
177
+
178
+
179
+ # Error body signatures for provider detection when keys are invalid.
180
+ # Each provider maps to a list of lowercase substrings found in their error responses.
181
+ PROVIDER_ERROR_SIGNATURES: dict[str, list[str]] = {
182
+ # ═══ 国内服务商 ═══
183
+ "dashscope": ["aliyun", "model-studio", "modelstudio", "apikey-error"],
184
+ "dashscope-coding": ["aliyun", "model-studio", "modelstudio"],
185
+ "tencent-hunyuan": ["hunyuan", "console.cloud.tencent.com"],
186
+ "baichuan": ["baichuan-ai.com", "platform.baichuan-ai.com"],
187
+ "minimax": ["authorized_error", "login fail"],
188
+ "minimax-plan": ["authorized_error", "login fail"],
189
+ "yi": ["illegal apikey"],
190
+ "kimi": ["invalid_authentication_error"],
191
+ "kimi-coding": ["invalid_authentication_error", "the api key appears to be invalid"],
192
+ "siliconflow": ["api key is invalid"],
193
+ "stepfun": ["incorrect api key provided", "invalid_api_key"],
194
+ "doubao": ["authenticationerror"],
195
+ "infini": ["请使用正确的api key进行请求"],
196
+ "infini-coding": ["请使用正确的api key进行请求"],
197
+ "zhipu": ["令牌已过期或验证不正确"],
198
+ "zhipu-coding": ["令牌已过期或验证不正确"],
199
+ "mimo": ["invalid api key", "please provide valid api key"],
200
+ "mimo-plan": ["invalid api key", "please provide valid api key"],
201
+ "cstcloud": ["cstcloud", "zhongsuanyun"],
202
+ "modelscope": ["modelscope"],
203
+ "longcat": ["longcat"],
204
+ "ppio": ["ppio"],
205
+ # ═══ 国外服务商 ═══
206
+ "deepseek": ["authentication fails"],
207
+ "anthropic": ["request not allowed", "anthropic", "x-api-key"],
208
+ "openrouter": ["missing authentication header"],
209
+ "together": [],
210
+ "mistral": ["mistral", "la plateforme"],
211
+ "cohere": [],
212
+ "replicate": ["unauthenticated", "you did not pass a valid authentication token"],
213
+ "huggingface": ["huggingface", "hf_"],
214
+ "fireworks": ["fireworks", "accounts/fireworks"],
215
+ "perplexity": ["perplexity"],
216
+ # grok: 实际返回 "Incorrect API key provided: sk***45...console.x.ai."
217
+ "grok": ["console.x.ai"],
218
+ "cerebras": ["cerebras"],
219
+ "nvidia": ["nvidia", "nim.api"],
220
+ "hyperbolic": ["could not validate credentials"],
221
+ "poe": ["poe.com"],
222
+ "ai302": ["302.ai"],
223
+ "dmxapi": ["rix_api_error"],
224
+ "ocoolai": ["shell_api_error"],
225
+ "zai": ["token expired or incorrect"],
226
+ # openai: 实际返回 "Incorrect API key provided: sk-inval...platform.openai.com..."
227
+ "openai": ["platform.openai.com"],
228
+ "google": ["generativelanguage"],
229
+ # grok: 实际返回 "Incorrect API key provided: sk***45...console.x.ai."
230
+ "groq": ["groq"],
231
+ }
232
+
233
+
234
+
235
+
236
+ # Provider website and documentation URLs
237
+ PROVIDER_WEBSITES: dict[str, dict[str, str]] = {
238
+ "openai": {"name": "OpenAI", "url": "https://platform.openai.com", "docs": "https://platform.openai.com/docs"},
239
+ "anthropic": {"name": "Anthropic", "url": "https://console.anthropic.com", "docs": "https://docs.anthropic.com"},
240
+ "google": {"name": "Google AI", "url": "https://aistudio.google.com", "docs": "https://ai.google.dev/docs"},
241
+ "deepseek": {"name": "DeepSeek", "url": "https://platform.deepseek.com", "docs": "https://platform.deepseek.com/api-docs"},
242
+ "groq": {"name": "Groq", "url": "https://console.groq.com", "docs": "https://docs.groq.com"},
243
+ "mistral": {"name": "Mistral AI", "url": "https://console.mistral.ai", "docs": "https://docs.mistral.ai"},
244
+ "cohere": {"name": "Cohere", "url": "https://dashboard.cohere.com", "docs": "https://docs.cohere.com"},
245
+ "replicate": {"name": "Replicate", "url": "https://replicate.com", "docs": "https://replicate.com/docs"},
246
+ "huggingface": {"name": "Hugging Face", "url": "https://huggingface.co", "docs": "https://huggingface.co/docs"},
247
+ "fireworks": {"name": "Fireworks AI", "url": "https://fireworks.ai", "docs": "https://docs.fireworks.ai"},
248
+ "perplexity": {"name": "Perplexity", "url": "https://perplexity.ai", "docs": "https://docs.perplexity.ai"},
249
+ "together": {"name": "Together AI", "url": "https://api.together.xyz", "docs": "https://docs.together.ai"},
250
+ "openrouter": {"name": "OpenRouter", "url": "https://openrouter.ai", "docs": "https://openrouter.ai/docs"},
251
+ "dashscope": {"name": "阿里百炼", "url": "https://dashscope.aliyun.com", "docs": "https://help.aliyun.com/zh/dashscope/"},
252
+ "zhipu": {"name": "智谱 AI", "url": "https://open.bigmodel.cn", "docs": "https://open.bigmodel.cn/dev/api"},
253
+ "kimi": {"name": "Kimi", "url": "https://platform.moonshot.cn", "docs": "https://platform.moonshot.cn/docs"},
254
+ "minimax": {"name": "MiniMax", "url": "https://platform.minimaxi.com", "docs": "https://platform.minimaxi.com/document"},
255
+ "siliconflow": {"name": "硅基流动", "url": "https://siliconflow.cn", "docs": "https://docs.siliconflow.cn"},
256
+ "baichuan": {"name": "百川智能", "url": "https://platform.baichuan-ai.com", "docs": "https://platform.baichuan-ai.com/docs"},
257
+ "yi": {"name": "零一万物", "url": "https://platform.lingyiwanwu.com", "docs": "https://platform.lingyiwanwu.com/docs"},
258
+ "cerebras": {"name": "Cerebras", "url": "https://cerebras.ai", "docs": "https://docs.cerebras.ai"},
259
+ "nvidia": {"name": "NVIDIA", "url": "https://build.nvidia.com", "docs": "https://docs.api.nvidia.com"},
260
+ "grok": {"name": "Grok (xAI)", "url": "https://console.x.ai", "docs": "https://docs.x.ai"},
261
+ "poe": {"name": "Poe", "url": "https://poe.com", "docs": "https://developer.poe.com"},
262
+ "stepfun": {"name": "阶跃星辰", "url": "https://platform.stepfun.com", "docs": "https://platform.stepfun.com/docs"},
263
+ "doubao": {"name": "豆包", "url": "https://console.volcengine.com/ark", "docs": "https://www.volcengine.com/docs/82379"},
264
+ "infini": {"name": "无问芯穹", "url": "https://cloud.infini-ai.com", "docs": "https://docs.infini-ai.com"},
265
+ "mimo": {"name": "MiMo", "url": "https://mimo.xiaomi.com", "docs": "https://mimo.xiaomi.com/docs"},
266
+ "hyperbolic": {"name": "Hyperbolic", "url": "https://hyperbolic.xyz", "docs": "https://docs.hyperbolic.xyz"},
267
+ "modelscope": {"name": "魔搭", "url": "https://modelscope.cn", "docs": "https://modelscope.cn/docs"},
268
+ "ppio": {"name": "PPIO", "url": "https://ppinfra.com", "docs": "https://docs.ppinfra.com"},
269
+ "dmxapi": {"name": "DMXAPI", "url": "https://www.dmxapi.cn", "docs": "https://www.dmxapi.cn/docs"},
270
+ "ocoolai": {"name": "OCoolAI", "url": "https://ocoolai.com", "docs": "https://ocoolai.com/docs"},
271
+ "ai302": {"name": "AI302", "url": "https://302.ai", "docs": "https://302.ai/docs"},
272
+ "zai": {"name": "ZAI", "url": "https://zai.ai", "docs": "https://zai.ai/docs"},
273
+ "longcat": {"name": "LongCat", "url": "https://longcat.com", "docs": "https://longcat.com/docs"},
274
+ "tencent-hunyuan": {"name": "腾讯混元", "url": "https://cloud.tencent.com/product/hunyuan", "docs": "https://cloud.tencent.com/document/product/1729"},
275
+ "cstcloud": {"name": "中算云", "url": "https://www.cstcloud.com", "docs": "https://www.cstcloud.com/docs"},
276
+ "dashscope-coding": {"name": "阿里百炼编程", "url": "https://dashscope.aliyun.com", "docs": "https://help.aliyun.com/zh/dashscope/"},
277
+ "mimo-plan": {"name": "MiMo 计划版", "url": "https://mimo.xiaomi.com", "docs": "https://mimo.xiaomi.com/docs"},
278
+ "minimax-plan": {"name": "MiniMax 计划版", "url": "https://platform.minimaxi.com", "docs": "https://platform.minimaxi.com/document"},
279
+ "zhipu-coding": {"name": "智谱 GLM 编程版", "url": "https://open.bigmodel.cn", "docs": "https://open.bigmodel.cn/dev/api"},
280
+ "kimi-coding": {"name": "Kimi 编程版", "url": "https://platform.moonshot.cn", "docs": "https://platform.moonshot.cn/docs"},
281
+ "infini-coding": {"name": "无问芯穹 编程版", "url": "https://cloud.infini-ai.com", "docs": "https://docs.infini-ai.com"},
282
+ }
283
+
@@ -0,0 +1,109 @@
1
+ import asyncio
2
+ import time
3
+ from .base import ProviderBase, CheckResult, TestResult
4
+
5
+
6
+ class AI302Provider(ProviderBase):
7
+ name = "ai302"
8
+ base_url = "https://api.302.ai/v1"
9
+ check_endpoint = "/models"
10
+
11
+ def build_headers(self, key: str) -> dict:
12
+ return {"Authorization": f"Bearer {key}"}
13
+
14
+ async def get_models(self, client, key: str) -> list[str]:
15
+ headers = self.build_headers(key)
16
+ try:
17
+ resp = await client.get(
18
+ f"{self.get_base_url()}{self.check_endpoint}",
19
+ headers=headers
20
+ )
21
+ if resp.status_code == 200:
22
+ data = resp.json()
23
+ if "data" in data:
24
+ return [m["id"] for m in data["data"] if "id" in m]
25
+ return []
26
+ except Exception:
27
+ return []
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
+ async def test_token_limit(self, client, key: str, token_steps: list[int]) -> TestResult:
63
+ headers = self.build_headers(key)
64
+ last_success = None
65
+ for step in token_steps:
66
+ try:
67
+ resp = await client.post(
68
+ f"{self.get_base_url()}/chat/completions",
69
+ headers=headers,
70
+ json={
71
+ "model": "gpt-4o-mini",
72
+ "messages": [{"role": "user", "content": "hi"}],
73
+ "max_tokens": step
74
+ }
75
+ )
76
+ if resp.status_code == 200:
77
+ last_success = step
78
+ elif resp.status_code in (400, 413):
79
+ break
80
+ elif resp.status_code == 429:
81
+ await asyncio.sleep(1)
82
+ continue
83
+ else:
84
+ break
85
+ except Exception:
86
+ break
87
+ return TestResult(max_tokens=last_success)
88
+
89
+
90
+ async def check_real(self, client, key: str) -> CheckResult:
91
+ return await self.check(client, key)
92
+ async def test_concurrency(self, client, key: str, concurrency_steps: list[int]) -> TestResult:
93
+ headers = self.build_headers(key)
94
+ last_success = None
95
+ for step in concurrency_steps:
96
+ tasks = [self._probe(client, headers) for _ in range(step)]
97
+ results = await asyncio.gather(*tasks, return_exceptions=True)
98
+ rate_limited = sum(1 for r in results if not isinstance(r, Exception) and not r)
99
+ if rate_limited / step >= 0.3:
100
+ break
101
+ last_success = step
102
+ return TestResult(max_concurrency=last_success)
103
+
104
+ async def _probe(self, client, headers) -> bool:
105
+ try:
106
+ resp = await client.get(f"{self.get_base_url()}{self.check_endpoint}", headers=headers)
107
+ return resp.status_code == 200
108
+ except Exception:
109
+ return False
@@ -0,0 +1,109 @@
1
+ import asyncio
2
+ import time
3
+ from .base import ProviderBase, CheckResult, TestResult
4
+
5
+
6
+ class AnthropicProvider(ProviderBase):
7
+ name = "anthropic"
8
+ base_url = "https://api.anthropic.com"
9
+ check_endpoint = "/v1/models"
10
+
11
+ def build_headers(self, key: str) -> dict:
12
+ return {
13
+ "x-api-key": key,
14
+ "anthropic-version": "2023-06-01"
15
+ }
16
+
17
+ async def get_models(self, client, key: str) -> list[str]:
18
+ headers = self.build_headers(key)
19
+ try:
20
+ resp = await client.get(
21
+ f"{self.get_base_url()}{self.check_endpoint}",
22
+ headers=headers
23
+ )
24
+ if resp.status_code == 200:
25
+ data = resp.json()
26
+ if "data" in data:
27
+ return [m["id"] for m in data["data"] if "id" in m]
28
+ return []
29
+ except Exception:
30
+ return []
31
+
32
+ async def check(self, client, key: str) -> CheckResult:
33
+ """Real usage test - try to make a minimal messages request."""
34
+ headers = self.build_headers(key)
35
+ headers["Content-Type"] = "application/json"
36
+ start = time.monotonic()
37
+ try:
38
+ resp = await client.post(
39
+ f"{self.get_base_url()}/v1/messages",
40
+ headers=headers,
41
+ json={
42
+ "model": "claude-3-haiku-20240307",
43
+ "messages": [{"role": "user", "content": "hi"}],
44
+ "max_tokens": 5
45
+ }
46
+ )
47
+ latency = (time.monotonic() - start) * 1000
48
+
49
+ if resp.status_code == 200:
50
+ return CheckResult(True, 200, latency, None)
51
+ elif resp.status_code in (401, 403):
52
+ return CheckResult(False, resp.status_code, latency, "invalid key or forbidden")
53
+ elif resp.status_code == 429:
54
+ return CheckResult(False, 429, latency, "rate limited")
55
+ else:
56
+ data = resp.json()
57
+ error_msg = data.get("error", {}).get("message", 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
+ async def test_token_limit(self, client, key: str, token_steps: list[int]) -> TestResult:
63
+ headers = self.build_headers(key)
64
+ last_success = None
65
+ for step in token_steps:
66
+ try:
67
+ resp = await client.post(
68
+ f"{self.get_base_url()}/v1/messages",
69
+ headers=headers,
70
+ json={
71
+ "model": "claude-3-haiku-20240307",
72
+ "messages": [{"role": "user", "content": "hi"}],
73
+ "max_tokens": step
74
+ }
75
+ )
76
+ if resp.status_code == 200:
77
+ last_success = step
78
+ elif resp.status_code in (400, 413):
79
+ break
80
+ elif resp.status_code == 429:
81
+ await asyncio.sleep(1)
82
+ continue
83
+ else:
84
+ break
85
+ except Exception:
86
+ break
87
+ return TestResult(max_tokens=last_success)
88
+
89
+
90
+ async def check_real(self, client, key: str) -> CheckResult:
91
+ return await self.check(client, key)
92
+ async def test_concurrency(self, client, key: str, concurrency_steps: list[int]) -> TestResult:
93
+ headers = self.build_headers(key)
94
+ last_success = None
95
+ for step in concurrency_steps:
96
+ tasks = [self._probe(client, headers) for _ in range(step)]
97
+ results = await asyncio.gather(*tasks, return_exceptions=True)
98
+ rate_limited = sum(1 for r in results if not isinstance(r, Exception) and not r)
99
+ if rate_limited / step >= 0.3:
100
+ break
101
+ last_success = step
102
+ return TestResult(max_concurrency=last_success)
103
+
104
+ async def _probe(self, client, headers) -> bool:
105
+ try:
106
+ resp = await client.get(f"{self.get_base_url()}{self.check_endpoint}", headers=headers)
107
+ return resp.status_code == 200
108
+ except Exception:
109
+ return False