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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from .base import ProviderBase, CheckResult, TestResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HyperbolicProvider(ProviderBase):
|
|
7
|
+
name = "hyperbolic"
|
|
8
|
+
base_url = "https://api.hyperbolic.xyz/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": "meta-llama/Llama-3.2-3B-Instruct",
|
|
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": "meta-llama/Llama-3.2-3B-Instruct",
|
|
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,135 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from .base import ProviderBase, CheckResult, TestResult, BalanceResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InfiniProvider(ProviderBase):
|
|
7
|
+
name = "infini"
|
|
8
|
+
base_url = "https://cloud.infini-ai.com/maas/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": "Qwen2.5-7B-Instruct",
|
|
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": "Qwen2.5-7B-Instruct",
|
|
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
|
|
110
|
+
|
|
111
|
+
async def get_balance(self, client, key: str) -> BalanceResult:
|
|
112
|
+
"""Get usage info via Infini coding usage API."""
|
|
113
|
+
headers = self.build_headers(key)
|
|
114
|
+
try:
|
|
115
|
+
resp = await client.get(
|
|
116
|
+
f"https://cloud.infini-ai.com/maas/coding/usage",
|
|
117
|
+
headers=headers
|
|
118
|
+
)
|
|
119
|
+
if resp.status_code == 200:
|
|
120
|
+
data = resp.json()
|
|
121
|
+
# Extract remaining quota from 30_day window
|
|
122
|
+
usage = data.get("30_day", {})
|
|
123
|
+
remain = usage.get("remain", 0)
|
|
124
|
+
return BalanceResult(
|
|
125
|
+
supported=True,
|
|
126
|
+
balance=float(remain),
|
|
127
|
+
currency="tokens",
|
|
128
|
+
raw=data,
|
|
129
|
+
)
|
|
130
|
+
elif resp.status_code in (401, 403):
|
|
131
|
+
return BalanceResult(supported=True, error="invalid key or forbidden")
|
|
132
|
+
else:
|
|
133
|
+
return BalanceResult(supported=True, error=f"status {resp.status_code}")
|
|
134
|
+
except Exception as e:
|
|
135
|
+
return BalanceResult(supported=True, error=str(e))
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from .base import ProviderBase, CheckResult, TestResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InfiniCodingProvider(ProviderBase):
|
|
7
|
+
"""无问芯穹 Coding Plan provider."""
|
|
8
|
+
name = "infini-coding"
|
|
9
|
+
base_url = "https://cloud.infini-ai.com/maas/coding/v1"
|
|
10
|
+
check_endpoint = "/models"
|
|
11
|
+
check_model = "Qwen2.5-7B-Instruct"
|
|
12
|
+
|
|
13
|
+
def build_headers(self, key: str) -> dict:
|
|
14
|
+
return {"Authorization": f"Bearer {key}"}
|
|
15
|
+
|
|
16
|
+
async def get_models(self, client, key: str) -> list[str]:
|
|
17
|
+
headers = self.build_headers(key)
|
|
18
|
+
try:
|
|
19
|
+
resp = await client.get(f"{self.get_base_url()}/models", headers=headers)
|
|
20
|
+
if resp.status_code == 200:
|
|
21
|
+
data = resp.json()
|
|
22
|
+
if "data" in data:
|
|
23
|
+
return [m["id"] for m in data["data"] if "id" in m]
|
|
24
|
+
return []
|
|
25
|
+
except Exception:
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
async def check(self, client, key: str) -> CheckResult:
|
|
29
|
+
headers = self.build_headers(key)
|
|
30
|
+
headers["Content-Type"] = "application/json"
|
|
31
|
+
start = time.monotonic()
|
|
32
|
+
try:
|
|
33
|
+
resp = await client.post(
|
|
34
|
+
f"{self.get_base_url()}/chat/completions",
|
|
35
|
+
headers=headers,
|
|
36
|
+
json={"model": "Qwen2.5-7B-Instruct", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
|
|
37
|
+
)
|
|
38
|
+
latency = (time.monotonic() - start) * 1000
|
|
39
|
+
if resp.status_code == 200:
|
|
40
|
+
return CheckResult(True, 200, latency, None)
|
|
41
|
+
elif resp.status_code in (401, 403):
|
|
42
|
+
return CheckResult(False, resp.status_code, latency, "invalid key or forbidden")
|
|
43
|
+
elif resp.status_code == 429:
|
|
44
|
+
return CheckResult(False, 429, latency, "rate limited")
|
|
45
|
+
else:
|
|
46
|
+
try:
|
|
47
|
+
data = resp.json()
|
|
48
|
+
error_msg = data.get("error", {}).get("message", f"status {resp.status_code}")
|
|
49
|
+
except:
|
|
50
|
+
error_msg = f"status {resp.status_code}"
|
|
51
|
+
return CheckResult(False, resp.status_code, latency, error_msg)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
return CheckResult(False, None, (time.monotonic() - start) * 1000, str(e))
|
|
54
|
+
|
|
55
|
+
async def test_token_limit(self, client, key: str, token_steps: list[int]) -> TestResult:
|
|
56
|
+
headers = self.build_headers(key)
|
|
57
|
+
last_success = None
|
|
58
|
+
for step in token_steps:
|
|
59
|
+
try:
|
|
60
|
+
resp = await client.post(
|
|
61
|
+
f"{self.get_base_url()}/chat/completions",
|
|
62
|
+
headers=headers,
|
|
63
|
+
json={"model": "Qwen2.5-7B-Instruct", "messages": [{"role": "user", "content": "hi"}], "max_tokens": step}
|
|
64
|
+
)
|
|
65
|
+
if resp.status_code == 200:
|
|
66
|
+
last_success = step
|
|
67
|
+
elif resp.status_code in (400, 413):
|
|
68
|
+
break
|
|
69
|
+
elif resp.status_code == 429:
|
|
70
|
+
await asyncio.sleep(1)
|
|
71
|
+
continue
|
|
72
|
+
else:
|
|
73
|
+
break
|
|
74
|
+
except Exception:
|
|
75
|
+
break
|
|
76
|
+
return TestResult(max_tokens=last_success)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def check_real(self, client, key: str) -> CheckResult:
|
|
80
|
+
return await self.check(client, key)
|
|
81
|
+
async def test_concurrency(self, client, key: str, concurrency_steps: list[int]) -> TestResult:
|
|
82
|
+
headers = self.build_headers(key)
|
|
83
|
+
last_success = None
|
|
84
|
+
for step in concurrency_steps:
|
|
85
|
+
tasks = [self._probe(client, headers) for _ in range(step)]
|
|
86
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
87
|
+
rate_limited = sum(1 for r in results if not isinstance(r, Exception) and not r)
|
|
88
|
+
if rate_limited / step >= 0.3:
|
|
89
|
+
break
|
|
90
|
+
last_success = step
|
|
91
|
+
return TestResult(max_concurrency=last_success)
|
|
92
|
+
|
|
93
|
+
async def _probe(self, client, headers) -> bool:
|
|
94
|
+
try:
|
|
95
|
+
resp = await client.get(f"{self.get_base_url()}{self.check_endpoint}", headers=headers)
|
|
96
|
+
return resp.status_code == 200
|
|
97
|
+
except Exception:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
async def test_concurrency_for_model(self, client, key: str, model: str, concurrency_steps: list[int]) -> TestResult:
|
|
101
|
+
"""Test concurrency for a specific model using chat completions."""
|
|
102
|
+
headers = self.build_headers(key)
|
|
103
|
+
headers["Content-Type"] = "application/json"
|
|
104
|
+
last_success = None
|
|
105
|
+
for step in concurrency_steps:
|
|
106
|
+
tasks = [self._probe_model(client, headers, model) for _ in range(step)]
|
|
107
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
108
|
+
rate_limited = sum(1 for r in results if not isinstance(r, Exception) and not r)
|
|
109
|
+
if rate_limited / step >= 0.3:
|
|
110
|
+
break
|
|
111
|
+
last_success = step
|
|
112
|
+
return TestResult(max_concurrency=last_success)
|
|
113
|
+
|
|
114
|
+
async def _probe_model(self, client, headers, model: str) -> bool:
|
|
115
|
+
"""Probe a specific model with a minimal chat completion request."""
|
|
116
|
+
try:
|
|
117
|
+
resp = await client.post(
|
|
118
|
+
f"{self.get_base_url()}/chat/completions",
|
|
119
|
+
headers=headers,
|
|
120
|
+
json={"model": model, "messages": [{"role": "user", "content": "hi"}], "max_tokens": 1}
|
|
121
|
+
)
|
|
122
|
+
return resp.status_code == 200
|
|
123
|
+
except Exception:
|
|
124
|
+
return False
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from .base import ProviderBase, CheckResult, TestResult, BalanceResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class KimiProvider(ProviderBase):
|
|
7
|
+
name = "kimi"
|
|
8
|
+
base_url = "https://api.moonshot.cn/v1"
|
|
9
|
+
check_endpoint = "/models"
|
|
10
|
+
check_model = "moonshot-v1-8k"
|
|
11
|
+
|
|
12
|
+
def build_headers(self, key: str) -> dict:
|
|
13
|
+
return {"Authorization": f"Bearer {key}"}
|
|
14
|
+
|
|
15
|
+
async def get_models(self, client, key: str) -> list[str]:
|
|
16
|
+
headers = self.build_headers(key)
|
|
17
|
+
try:
|
|
18
|
+
resp = await client.get(f"{self.get_base_url()}/models", headers=headers)
|
|
19
|
+
if resp.status_code == 200:
|
|
20
|
+
data = resp.json()
|
|
21
|
+
if "data" in data:
|
|
22
|
+
return [m["id"] for m in data["data"] if "id" in m]
|
|
23
|
+
return []
|
|
24
|
+
except Exception:
|
|
25
|
+
return []
|
|
26
|
+
|
|
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": "moonshot-v1-8k", "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
|
+
async def test_token_limit(self, client, key: str, token_steps: list[int]) -> TestResult:
|
|
55
|
+
headers = self.build_headers(key)
|
|
56
|
+
last_success = None
|
|
57
|
+
for step in token_steps:
|
|
58
|
+
try:
|
|
59
|
+
resp = await client.post(
|
|
60
|
+
f"{self.get_base_url()}/chat/completions",
|
|
61
|
+
headers=headers,
|
|
62
|
+
json={"model": "moonshot-v1-8k", "messages": [{"role": "user", "content": "hi"}], "max_tokens": step}
|
|
63
|
+
)
|
|
64
|
+
if resp.status_code == 200:
|
|
65
|
+
last_success = step
|
|
66
|
+
elif resp.status_code in (400, 413):
|
|
67
|
+
break
|
|
68
|
+
elif resp.status_code == 429:
|
|
69
|
+
await asyncio.sleep(1)
|
|
70
|
+
continue
|
|
71
|
+
else:
|
|
72
|
+
break
|
|
73
|
+
except Exception:
|
|
74
|
+
break
|
|
75
|
+
return TestResult(max_tokens=last_success)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def check_real(self, client, key: str) -> CheckResult:
|
|
79
|
+
return await self.check(client, key)
|
|
80
|
+
async def test_concurrency(self, client, key: str, concurrency_steps: list[int]) -> TestResult:
|
|
81
|
+
headers = self.build_headers(key)
|
|
82
|
+
last_success = None
|
|
83
|
+
for step in concurrency_steps:
|
|
84
|
+
tasks = [self._probe(client, headers) for _ in range(step)]
|
|
85
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
86
|
+
rate_limited = sum(1 for r in results if not isinstance(r, Exception) and not r)
|
|
87
|
+
if rate_limited / step >= 0.3:
|
|
88
|
+
break
|
|
89
|
+
last_success = step
|
|
90
|
+
return TestResult(max_concurrency=last_success)
|
|
91
|
+
|
|
92
|
+
async def _probe(self, client, headers) -> bool:
|
|
93
|
+
try:
|
|
94
|
+
resp = await client.get(f"{self.get_base_url()}{self.check_endpoint}", headers=headers)
|
|
95
|
+
return resp.status_code == 200
|
|
96
|
+
except Exception:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
async def get_balance(self, client, key: str) -> BalanceResult:
|
|
100
|
+
"""Get account balance via Kimi balance API."""
|
|
101
|
+
headers = self.build_headers(key)
|
|
102
|
+
try:
|
|
103
|
+
resp = await client.get(
|
|
104
|
+
f"{self.get_base_url()}/users/me/balance",
|
|
105
|
+
headers=headers
|
|
106
|
+
)
|
|
107
|
+
if resp.status_code == 200:
|
|
108
|
+
data = resp.json()
|
|
109
|
+
balance_data = data.get("data", {})
|
|
110
|
+
return BalanceResult(
|
|
111
|
+
supported=True,
|
|
112
|
+
balance=float(balance_data.get("available_balance", 0)),
|
|
113
|
+
currency="CNY",
|
|
114
|
+
raw=data,
|
|
115
|
+
)
|
|
116
|
+
elif resp.status_code in (401, 403):
|
|
117
|
+
return BalanceResult(supported=True, error="invalid key or forbidden")
|
|
118
|
+
else:
|
|
119
|
+
return BalanceResult(supported=True, error=f"status {resp.status_code}")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
return BalanceResult(supported=True, error=str(e))
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from .base import ProviderBase, CheckResult, TestResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class KimiCodingProvider(ProviderBase):
|
|
7
|
+
"""Kimi Coding Plan provider."""
|
|
8
|
+
name = "kimi-coding"
|
|
9
|
+
base_url = "https://api.kimi.com/coding/v1"
|
|
10
|
+
check_endpoint = "/models"
|
|
11
|
+
check_model = "kimi-latest"
|
|
12
|
+
|
|
13
|
+
def build_headers(self, key: str) -> dict:
|
|
14
|
+
return {"Authorization": f"Bearer {key}"}
|
|
15
|
+
|
|
16
|
+
async def get_models(self, client, key: str) -> list[str]:
|
|
17
|
+
headers = self.build_headers(key)
|
|
18
|
+
try:
|
|
19
|
+
resp = await client.get(f"{self.get_base_url()}/models", headers=headers)
|
|
20
|
+
if resp.status_code == 200:
|
|
21
|
+
data = resp.json()
|
|
22
|
+
if "data" in data:
|
|
23
|
+
return [m["id"] for m in data["data"] if "id" in m]
|
|
24
|
+
return []
|
|
25
|
+
except Exception:
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
async def check(self, client, key: str) -> CheckResult:
|
|
29
|
+
headers = self.build_headers(key)
|
|
30
|
+
headers["Content-Type"] = "application/json"
|
|
31
|
+
start = time.monotonic()
|
|
32
|
+
try:
|
|
33
|
+
resp = await client.post(
|
|
34
|
+
f"{self.get_base_url()}/chat/completions",
|
|
35
|
+
headers=headers,
|
|
36
|
+
json={"model": "kimi-latest", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}
|
|
37
|
+
)
|
|
38
|
+
latency = (time.monotonic() - start) * 1000
|
|
39
|
+
if resp.status_code == 200:
|
|
40
|
+
return CheckResult(True, 200, latency, None)
|
|
41
|
+
elif resp.status_code in (401, 403):
|
|
42
|
+
return CheckResult(False, resp.status_code, latency, "invalid key or forbidden")
|
|
43
|
+
elif resp.status_code == 429:
|
|
44
|
+
return CheckResult(False, 429, latency, "rate limited")
|
|
45
|
+
else:
|
|
46
|
+
try:
|
|
47
|
+
data = resp.json()
|
|
48
|
+
error_msg = data.get("error", {}).get("message", f"status {resp.status_code}")
|
|
49
|
+
except:
|
|
50
|
+
error_msg = f"status {resp.status_code}"
|
|
51
|
+
return CheckResult(False, resp.status_code, latency, error_msg)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
return CheckResult(False, None, (time.monotonic() - start) * 1000, str(e))
|
|
54
|
+
|
|
55
|
+
async def test_token_limit(self, client, key: str, token_steps: list[int]) -> TestResult:
|
|
56
|
+
headers = self.build_headers(key)
|
|
57
|
+
last_success = None
|
|
58
|
+
for step in token_steps:
|
|
59
|
+
try:
|
|
60
|
+
resp = await client.post(
|
|
61
|
+
f"{self.get_base_url()}/chat/completions",
|
|
62
|
+
headers=headers,
|
|
63
|
+
json={"model": "kimi-latest", "messages": [{"role": "user", "content": "hi"}], "max_tokens": step}
|
|
64
|
+
)
|
|
65
|
+
if resp.status_code == 200:
|
|
66
|
+
last_success = step
|
|
67
|
+
elif resp.status_code in (400, 413):
|
|
68
|
+
break
|
|
69
|
+
elif resp.status_code == 429:
|
|
70
|
+
await asyncio.sleep(1)
|
|
71
|
+
continue
|
|
72
|
+
else:
|
|
73
|
+
break
|
|
74
|
+
except Exception:
|
|
75
|
+
break
|
|
76
|
+
return TestResult(max_tokens=last_success)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def check_real(self, client, key: str) -> CheckResult:
|
|
80
|
+
return await self.check(client, key)
|
|
81
|
+
async def test_concurrency(self, client, key: str, concurrency_steps: list[int]) -> TestResult:
|
|
82
|
+
headers = self.build_headers(key)
|
|
83
|
+
last_success = None
|
|
84
|
+
for step in concurrency_steps:
|
|
85
|
+
tasks = [self._probe(client, headers) for _ in range(step)]
|
|
86
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
87
|
+
rate_limited = sum(1 for r in results if not isinstance(r, Exception) and not r)
|
|
88
|
+
if rate_limited / step >= 0.3:
|
|
89
|
+
break
|
|
90
|
+
last_success = step
|
|
91
|
+
return TestResult(max_concurrency=last_success)
|
|
92
|
+
|
|
93
|
+
async def _probe(self, client, headers) -> bool:
|
|
94
|
+
try:
|
|
95
|
+
resp = await client.get(f"{self.get_base_url()}{self.check_endpoint}", headers=headers)
|
|
96
|
+
return resp.status_code == 200
|
|
97
|
+
except Exception:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
async def test_concurrency_for_model(self, client, key: str, model: str, concurrency_steps: list[int]) -> TestResult:
|
|
101
|
+
"""Test concurrency for a specific model using chat completions."""
|
|
102
|
+
headers = self.build_headers(key)
|
|
103
|
+
headers["Content-Type"] = "application/json"
|
|
104
|
+
last_success = None
|
|
105
|
+
for step in concurrency_steps:
|
|
106
|
+
tasks = [self._probe_model(client, headers, model) for _ in range(step)]
|
|
107
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
108
|
+
rate_limited = sum(1 for r in results if not isinstance(r, Exception) and not r)
|
|
109
|
+
if rate_limited / step >= 0.3:
|
|
110
|
+
break
|
|
111
|
+
last_success = step
|
|
112
|
+
return TestResult(max_concurrency=last_success)
|
|
113
|
+
|
|
114
|
+
async def _probe_model(self, client, headers, model: str) -> bool:
|
|
115
|
+
"""Probe a specific model with a minimal chat completion request."""
|
|
116
|
+
try:
|
|
117
|
+
resp = await client.post(
|
|
118
|
+
f"{self.get_base_url()}/chat/completions",
|
|
119
|
+
headers=headers,
|
|
120
|
+
json={"model": model, "messages": [{"role": "user", "content": "hi"}], "max_tokens": 1}
|
|
121
|
+
)
|
|
122
|
+
return resp.status_code == 200
|
|
123
|
+
except Exception:
|
|
124
|
+
return False
|