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
@@ -0,0 +1,122 @@
1
+ import asyncio
2
+ import time
3
+ from .base import ProviderBase, CheckResult, TestResult
4
+
5
+
6
+ class TencentHunyuanProvider(ProviderBase):
7
+ """腾讯混元 provider - OpenAI compatible endpoint."""
8
+ name = "tencent-hunyuan"
9
+ base_url = "https://api.hunyuan.cloud.tencent.com/v1"
10
+ check_endpoint = "/models"
11
+ check_model = "hunyuan-turbos-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()}{self.check_endpoint}", 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": "hunyuan-turbos-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": "hunyuan-turbos-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
+ headers = self.build_headers(key)
102
+ headers["Content-Type"] = "application/json"
103
+ last_success = None
104
+ for step in concurrency_steps:
105
+ tasks = [self._probe_model(client, headers, model) for _ in range(step)]
106
+ results = await asyncio.gather(*tasks, return_exceptions=True)
107
+ rate_limited = sum(1 for r in results if not isinstance(r, Exception) and not r)
108
+ if rate_limited / step >= 0.3:
109
+ break
110
+ last_success = step
111
+ return TestResult(max_concurrency=last_success)
112
+
113
+ async def _probe_model(self, client, headers, model: str) -> bool:
114
+ try:
115
+ resp = await client.post(
116
+ f"{self.get_base_url()}/chat/completions",
117
+ headers=headers,
118
+ json={"model": model, "messages": [{"role": "user", "content": "hi"}], "max_tokens": 1}
119
+ )
120
+ return resp.status_code == 200
121
+ except Exception:
122
+ return False
@@ -0,0 +1,134 @@
1
+ import asyncio
2
+ import time
3
+ from .base import ProviderBase, CheckResult, TestResult, BalanceResult
4
+
5
+
6
+ class TogetherProvider(ProviderBase):
7
+ name = "together"
8
+ base_url = "https://api.together.xyz"
9
+ check_endpoint = "/v1/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()}/v1/chat/completions",
37
+ headers=headers,
38
+ json={
39
+ "model": "mistralai/Mixtral-8x7B-v0.1",
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()}/v1/chat/completions",
69
+ headers=headers,
70
+ json={
71
+ "model": "mistralai/Mixtral-8x7B-v0.1",
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 organization info via Together API."""
113
+ headers = self.build_headers(key)
114
+ try:
115
+ resp = await client.get(
116
+ f"{self.get_base_url()}/v1/organizations/me",
117
+ headers=headers
118
+ )
119
+ if resp.status_code == 200:
120
+ data = resp.json()
121
+ # Together API returns org info, not direct balance
122
+ # Try to extract any balance-related fields
123
+ return BalanceResult(
124
+ supported=True,
125
+ balance=None,
126
+ currency="USD",
127
+ raw=data,
128
+ )
129
+ elif resp.status_code in (401, 403):
130
+ return BalanceResult(supported=True, error="invalid key or forbidden")
131
+ else:
132
+ return BalanceResult(supported=True, error=f"status {resp.status_code}")
133
+ except Exception as e:
134
+ return BalanceResult(supported=True, error=str(e))
@@ -0,0 +1,97 @@
1
+ import asyncio
2
+ import time
3
+ from .base import ProviderBase, CheckResult, TestResult
4
+
5
+
6
+ class YiProvider(ProviderBase):
7
+ name = "yi"
8
+ base_url = "https://api.lingyiwanwu.com/v1"
9
+ check_endpoint = "/models"
10
+ check_model = "yi-lightning"
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": "yi-lightning", "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": "yi-lightning", "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
@@ -0,0 +1,109 @@
1
+ import asyncio
2
+ import time
3
+ from .base import ProviderBase, CheckResult, TestResult
4
+
5
+
6
+ class ZAIProvider(ProviderBase):
7
+ name = "zai"
8
+ base_url = "https://api.z.ai/api/paas/v4"
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": "glm-4-flash",
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": "glm-4-flash",
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,127 @@
1
+ import asyncio
2
+ import time
3
+ from .base import ProviderBase, CheckResult, TestResult, BalanceResult
4
+
5
+
6
+ class ZhipuProvider(ProviderBase):
7
+ name = "zhipu"
8
+ base_url = "https://open.bigmodel.cn/api/paas/v4"
9
+ check_endpoint = "/models"
10
+ check_model = "glm-4-flash"
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": "glm-4-flash", "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": "glm-4-flash", "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 quota/usage info via Zhipu monitoring API."""
101
+ headers = self.build_headers(key)
102
+ try:
103
+ resp = await client.get(
104
+ f"https://open.bigmodel.cn/api/monitor/usage/quota/limit",
105
+ headers=headers
106
+ )
107
+ if resp.status_code == 200:
108
+ data = resp.json()
109
+ if data.get("success"):
110
+ limits = data.get("data", {}).get("limits", [])
111
+ # Extract percentage used from limits
112
+ for limit in limits:
113
+ if limit.get("type") == "TOKENS_LIMIT":
114
+ percentage = limit.get("percentage", 0)
115
+ return BalanceResult(
116
+ supported=True,
117
+ balance=float(100 - percentage), # remaining percentage
118
+ currency="%",
119
+ raw=data,
120
+ )
121
+ return BalanceResult(supported=True, balance=None, raw=data)
122
+ elif resp.status_code in (401, 403):
123
+ return BalanceResult(supported=True, error="invalid key or forbidden")
124
+ else:
125
+ return BalanceResult(supported=True, error=f"status {resp.status_code}")
126
+ except Exception as e:
127
+ return BalanceResult(supported=True, error=str(e))