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.
- api_key_manager-2.1.0.dist-info/METADATA +709 -0
- api_key_manager-2.1.0.dist-info/RECORD +73 -0
- api_key_manager-2.1.0.dist-info/WHEEL +5 -0
- api_key_manager-2.1.0.dist-info/entry_points.txt +2 -0
- api_key_manager-2.1.0.dist-info/top_level.txt +1 -0
- key_manager/__init__.py +16 -0
- key_manager/__main__.py +5 -0
- key_manager/api_models.py +358 -0
- key_manager/checker.py +51 -0
- key_manager/cli.py +270 -0
- key_manager/config.py +61 -0
- key_manager/core.py +205 -0
- key_manager/detector.py +335 -0
- key_manager/errors.py +179 -0
- key_manager/i18n.py +142 -0
- key_manager/logger.py +207 -0
- key_manager/model_capabilities.py +412 -0
- key_manager/parser.py +153 -0
- key_manager/providers/__init__.py +283 -0
- key_manager/providers/ai302.py +109 -0
- key_manager/providers/anthropic.py +109 -0
- key_manager/providers/baichuan.py +97 -0
- key_manager/providers/base.py +312 -0
- key_manager/providers/cerebras.py +109 -0
- key_manager/providers/cohere.py +90 -0
- key_manager/providers/cstcloud.py +122 -0
- key_manager/providers/dashscope.py +120 -0
- key_manager/providers/dashscope_coding.py +122 -0
- key_manager/providers/deepseek.py +166 -0
- key_manager/providers/dmxapi.py +109 -0
- key_manager/providers/doubao.py +109 -0
- key_manager/providers/fireworks.py +109 -0
- key_manager/providers/google.py +99 -0
- key_manager/providers/grok.py +109 -0
- key_manager/providers/groq.py +109 -0
- key_manager/providers/huggingface.py +54 -0
- key_manager/providers/hyperbolic.py +109 -0
- key_manager/providers/infini.py +135 -0
- key_manager/providers/infini_coding.py +124 -0
- key_manager/providers/kimi.py +121 -0
- key_manager/providers/kimi_coding.py +124 -0
- key_manager/providers/longcat.py +123 -0
- key_manager/providers/mimo.py +109 -0
- key_manager/providers/mimo_plan.py +140 -0
- key_manager/providers/minimax.py +97 -0
- key_manager/providers/minimax_plan.py +122 -0
- key_manager/providers/mistral.py +109 -0
- key_manager/providers/models_registry.py +2901 -0
- key_manager/providers/modelscope.py +134 -0
- key_manager/providers/nvidia.py +109 -0
- key_manager/providers/ocoolai.py +109 -0
- key_manager/providers/openai.py +140 -0
- key_manager/providers/openrouter.py +119 -0
- key_manager/providers/perplexity.py +109 -0
- key_manager/providers/poe.py +109 -0
- key_manager/providers/ppio.py +109 -0
- key_manager/providers/replicate.py +54 -0
- key_manager/providers/siliconflow.py +121 -0
- key_manager/providers/stepfun.py +132 -0
- key_manager/providers/tencent_hunyuan.py +122 -0
- key_manager/providers/together.py +134 -0
- key_manager/providers/yi.py +97 -0
- key_manager/providers/zai.py +109 -0
- key_manager/providers/zhipu.py +127 -0
- key_manager/providers/zhipu_coding.py +124 -0
- key_manager/proxy.py +70 -0
- key_manager/ssrf.py +68 -0
- key_manager/storage.py +134 -0
- key_manager/tester.py +137 -0
- key_manager/url_override.py +5 -0
- key_manager/validator.py +185 -0
- key_manager/web.py +1512 -0
- 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
|