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/cli.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from key_manager.config import load_config
|
|
12
|
+
from key_manager.parser import import_keys
|
|
13
|
+
from key_manager.validator import validate_keys
|
|
14
|
+
from key_manager.checker import run_check
|
|
15
|
+
from key_manager.tester import run_test
|
|
16
|
+
from key_manager.proxy import get_proxy
|
|
17
|
+
from key_manager.storage import KeyStore
|
|
18
|
+
from key_manager.errors import KeyManagerError, ErrorCode
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_keys(config: dict) -> dict:
|
|
24
|
+
"""Load keys data using KeyStore, falling back to direct JSON read."""
|
|
25
|
+
try:
|
|
26
|
+
return KeyStore(config["storage"]["keys_file"], config).load()
|
|
27
|
+
except Exception:
|
|
28
|
+
keys_path = Path(config["storage"]["keys_file"])
|
|
29
|
+
if not keys_path.exists():
|
|
30
|
+
return {"keys": {}}
|
|
31
|
+
with open(keys_path, "r", encoding="utf-8") as f:
|
|
32
|
+
return json.load(f)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def cmd_import(args, config):
|
|
36
|
+
new, dupes, errors = import_keys(
|
|
37
|
+
file_path=args.file,
|
|
38
|
+
directory=args.dir or config["scan"]["directories"][0],
|
|
39
|
+
batch=args.batch,
|
|
40
|
+
keys_file=config["storage"]["keys_file"]
|
|
41
|
+
)
|
|
42
|
+
console.print(f"[green]Import complete:[/green] {new} new, {dupes} duplicates")
|
|
43
|
+
for err in errors:
|
|
44
|
+
console.print(f"[red]Error:[/red] {err}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cmd_check(args, config):
|
|
48
|
+
proxy = get_proxy(config.get("proxy", ""))
|
|
49
|
+
if proxy:
|
|
50
|
+
console.print(f"[blue]Using proxy:[/blue] {proxy}")
|
|
51
|
+
results = asyncio.run(run_check(
|
|
52
|
+
keys_file=config["storage"]["keys_file"],
|
|
53
|
+
results_file=config["storage"]["check_results_file"],
|
|
54
|
+
logs_dir=config["storage"]["logs_dir"],
|
|
55
|
+
concurrency=config["check"]["concurrency"],
|
|
56
|
+
timeout=config["check"]["timeout_seconds"],
|
|
57
|
+
proxy=proxy or None,
|
|
58
|
+
retry_failed=config["check"]["retry_failed"],
|
|
59
|
+
retry_count=config["check"]["retry_count"]
|
|
60
|
+
))
|
|
61
|
+
console.print(f"\n[bold]Check Results[/bold]")
|
|
62
|
+
console.print(f"Total: {results['total']}")
|
|
63
|
+
console.print(f"Valid: {results['summary']['valid']['count']}")
|
|
64
|
+
console.print(f"Invalid: {results['summary']['invalid']['count']}")
|
|
65
|
+
console.print(f"Error: {results['summary']['error']['count']}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_test(args, config):
|
|
69
|
+
proxy = get_proxy(config.get("proxy", ""))
|
|
70
|
+
if proxy:
|
|
71
|
+
console.print(f"[blue]Using proxy:[/blue] {proxy}")
|
|
72
|
+
results = asyncio.run(run_test(
|
|
73
|
+
keys_file=config["storage"]["keys_file"],
|
|
74
|
+
results_file=config["storage"]["test_results_file"],
|
|
75
|
+
logs_dir=config["storage"]["logs_dir"],
|
|
76
|
+
timeout=config["test"]["concurrency_timeout_seconds"],
|
|
77
|
+
proxy=proxy or None,
|
|
78
|
+
token_test=not args.skip_token,
|
|
79
|
+
concurrency_test=not args.skip_concurrency,
|
|
80
|
+
token_steps=config["test"]["token_steps"],
|
|
81
|
+
concurrency_steps=config["test"]["concurrency_steps"],
|
|
82
|
+
provider_filter=args.provider,
|
|
83
|
+
single_key=args.key
|
|
84
|
+
))
|
|
85
|
+
console.print(f"\n[bold]Test Results[/bold]")
|
|
86
|
+
console.print(f"Total tested: {results['total_tested']}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cmd_list(args, config):
|
|
90
|
+
keys_path = Path(config["storage"]["keys_file"])
|
|
91
|
+
if not keys_path.exists():
|
|
92
|
+
console.print("[yellow]No keys file found. Run 'import' first.[/yellow]")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
data = _load_keys(config)
|
|
97
|
+
except KeyManagerError as e:
|
|
98
|
+
console.print(f"[red]Error:[/red] {e.message}")
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
table = Table(title="API Keys")
|
|
102
|
+
table.add_column("Key", style="cyan")
|
|
103
|
+
table.add_column("Provider", style="green")
|
|
104
|
+
table.add_column("Status", style="yellow")
|
|
105
|
+
table.add_column("Last Checked")
|
|
106
|
+
table.add_column("Max Token")
|
|
107
|
+
table.add_column("Concurrency")
|
|
108
|
+
table.add_column("Sources", justify="right")
|
|
109
|
+
|
|
110
|
+
for key, info in data["keys"].items():
|
|
111
|
+
if args.provider and info["provider"].lower() != args.provider.lower():
|
|
112
|
+
continue
|
|
113
|
+
if args.status and info["status"] != args.status:
|
|
114
|
+
continue
|
|
115
|
+
if args.batch:
|
|
116
|
+
has_batch = any(s.get("batch") == args.batch for s in info.get("sources", []))
|
|
117
|
+
if not has_batch:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
status_style = {
|
|
121
|
+
"valid": "[green]valid[/green]",
|
|
122
|
+
"invalid": "[red]invalid[/red]",
|
|
123
|
+
"error": "[red]error[/red]",
|
|
124
|
+
}.get(info["status"], f"[yellow]{info['status']}[/yellow]")
|
|
125
|
+
|
|
126
|
+
tests = info.get("tests", {})
|
|
127
|
+
max_tokens = str(tests.get("max_tokens", "-")) if tests.get("max_tokens") else "-"
|
|
128
|
+
max_conc = str(tests.get("max_concurrency", "-")) if tests.get("max_concurrency") else "-"
|
|
129
|
+
|
|
130
|
+
table.add_row(
|
|
131
|
+
info["key_masked"],
|
|
132
|
+
info["provider"],
|
|
133
|
+
status_style,
|
|
134
|
+
info.get("last_checked", "never") or "never",
|
|
135
|
+
max_tokens,
|
|
136
|
+
max_conc,
|
|
137
|
+
str(len(info.get("sources", [])))
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
console.print(table)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def cmd_report(args, config):
|
|
144
|
+
keys_path = Path(config["storage"]["keys_file"])
|
|
145
|
+
if not keys_path.exists():
|
|
146
|
+
console.print("[yellow]No keys file found.[/yellow]")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
data = _load_keys(config)
|
|
151
|
+
except KeyManagerError as e:
|
|
152
|
+
console.print(f"[red]Error:[/red] {e.message}")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
days = args.days or 7
|
|
156
|
+
cutoff = datetime.utcnow() - timedelta(days=days)
|
|
157
|
+
|
|
158
|
+
stats = {}
|
|
159
|
+
for key, info in data["keys"].items():
|
|
160
|
+
provider = info["provider"]
|
|
161
|
+
if provider not in stats:
|
|
162
|
+
stats[provider] = {"total": 0, "valid": 0, "invalid": 0, "error": 0, "max_tokens": [], "concurrency": []}
|
|
163
|
+
stats[provider]["total"] += 1
|
|
164
|
+
status = info.get("status", "unknown")
|
|
165
|
+
if status in stats[provider]:
|
|
166
|
+
stats[provider][status] += 1
|
|
167
|
+
tests = info.get("tests", {})
|
|
168
|
+
if tests.get("max_tokens"):
|
|
169
|
+
stats[provider]["max_tokens"].append(tests["max_tokens"])
|
|
170
|
+
if tests.get("max_concurrency"):
|
|
171
|
+
stats[provider]["concurrency"].append(tests["max_concurrency"])
|
|
172
|
+
|
|
173
|
+
table = Table(title=f"Key Statistics (last {days} days)")
|
|
174
|
+
table.add_column("Provider", style="green")
|
|
175
|
+
table.add_column("Total", justify="right")
|
|
176
|
+
table.add_column("Valid", justify="right", style="green")
|
|
177
|
+
table.add_column("Invalid", justify="right", style="red")
|
|
178
|
+
table.add_column("Error", justify="right", style="red")
|
|
179
|
+
table.add_column("MaxToken")
|
|
180
|
+
table.add_column("Concurrency")
|
|
181
|
+
|
|
182
|
+
total_all = 0
|
|
183
|
+
total_valid = 0
|
|
184
|
+
total_invalid = 0
|
|
185
|
+
total_error = 0
|
|
186
|
+
|
|
187
|
+
for provider, s in sorted(stats.items()):
|
|
188
|
+
total_all += s["total"]
|
|
189
|
+
total_valid += s["valid"]
|
|
190
|
+
total_invalid += s["invalid"]
|
|
191
|
+
total_error += s["error"]
|
|
192
|
+
|
|
193
|
+
max_tokens = max(s["max_tokens"]) if s["max_tokens"] else "-"
|
|
194
|
+
max_conc = max(s["concurrency"]) if s["concurrency"] else "-"
|
|
195
|
+
|
|
196
|
+
table.add_row(
|
|
197
|
+
provider,
|
|
198
|
+
str(s["total"]),
|
|
199
|
+
str(s["valid"]),
|
|
200
|
+
str(s["invalid"]),
|
|
201
|
+
str(s["error"]),
|
|
202
|
+
str(max_tokens),
|
|
203
|
+
str(max_conc)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
table.add_section()
|
|
207
|
+
table.add_row(
|
|
208
|
+
"TOTAL",
|
|
209
|
+
str(total_all),
|
|
210
|
+
str(total_valid),
|
|
211
|
+
str(total_invalid),
|
|
212
|
+
str(total_error),
|
|
213
|
+
"-", "-"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
console.print(table)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main():
|
|
220
|
+
parser = argparse.ArgumentParser(description="API Key Manager")
|
|
221
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
222
|
+
|
|
223
|
+
# Import command
|
|
224
|
+
import_parser = subparsers.add_parser("import", help="Import API keys")
|
|
225
|
+
import_parser.add_argument("--file", help="JSON file to import")
|
|
226
|
+
import_parser.add_argument("--dir", help="Directory to scan")
|
|
227
|
+
import_parser.add_argument("--batch", help="Batch label")
|
|
228
|
+
|
|
229
|
+
# Check command
|
|
230
|
+
check_parser = subparsers.add_parser("check", help="Validate keys")
|
|
231
|
+
check_parser.add_argument("--provider", help="Filter by provider")
|
|
232
|
+
check_parser.add_argument("--status", help="Filter by status")
|
|
233
|
+
check_parser.add_argument("--key", help="Check single key")
|
|
234
|
+
|
|
235
|
+
# Test command
|
|
236
|
+
test_parser = subparsers.add_parser("test", help="Token/concurrency test")
|
|
237
|
+
test_parser.add_argument("--skip-token", action="store_true", help="Skip token test")
|
|
238
|
+
test_parser.add_argument("--skip-concurrency", action="store_true", help="Skip concurrency test")
|
|
239
|
+
test_parser.add_argument("--provider", help="Filter by provider")
|
|
240
|
+
test_parser.add_argument("--key", help="Test single key")
|
|
241
|
+
|
|
242
|
+
# List command
|
|
243
|
+
list_parser = subparsers.add_parser("list", help="List keys")
|
|
244
|
+
list_parser.add_argument("--provider", help="Filter by provider")
|
|
245
|
+
list_parser.add_argument("--status", help="Filter by status")
|
|
246
|
+
list_parser.add_argument("--batch", help="Filter by batch")
|
|
247
|
+
|
|
248
|
+
# Report command
|
|
249
|
+
report_parser = subparsers.add_parser("report", help="Show statistics")
|
|
250
|
+
report_parser.add_argument("--days", type=int, help="Days to include")
|
|
251
|
+
|
|
252
|
+
args = parser.parse_args()
|
|
253
|
+
config = load_config()
|
|
254
|
+
|
|
255
|
+
if args.command == "import":
|
|
256
|
+
cmd_import(args, config)
|
|
257
|
+
elif args.command == "check":
|
|
258
|
+
cmd_check(args, config)
|
|
259
|
+
elif args.command == "test":
|
|
260
|
+
cmd_test(args, config)
|
|
261
|
+
elif args.command == "list":
|
|
262
|
+
cmd_list(args, config)
|
|
263
|
+
elif args.command == "report":
|
|
264
|
+
cmd_report(args, config)
|
|
265
|
+
else:
|
|
266
|
+
parser.print_help()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
main()
|
key_manager/config.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
DEFAULT_CONFIG = {
|
|
5
|
+
"scan": {
|
|
6
|
+
"directories": ["./data/input"],
|
|
7
|
+
"recursive": True,
|
|
8
|
+
},
|
|
9
|
+
"check": {
|
|
10
|
+
"interval_hours": 6,
|
|
11
|
+
"timeout_seconds": 30,
|
|
12
|
+
"concurrency": 100,
|
|
13
|
+
"retry_failed": True,
|
|
14
|
+
"retry_count": 2,
|
|
15
|
+
},
|
|
16
|
+
"test": {
|
|
17
|
+
"token_test": True,
|
|
18
|
+
"token_auto_detect": True,
|
|
19
|
+
"token_steps": [1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576],
|
|
20
|
+
"token_max_manual": None,
|
|
21
|
+
"concurrency_test": True,
|
|
22
|
+
"concurrency_steps": [1, 5, 10, 20, 50, 100],
|
|
23
|
+
"concurrency_timeout_seconds": 120,
|
|
24
|
+
},
|
|
25
|
+
"storage": {
|
|
26
|
+
"keys_file": "./data/keys.json",
|
|
27
|
+
"check_results_file": "./data/check_results.json",
|
|
28
|
+
"test_results_file": "./data/test_results.json",
|
|
29
|
+
"logs_dir": "./data/logs",
|
|
30
|
+
},
|
|
31
|
+
"auth": {
|
|
32
|
+
"api_key": "",
|
|
33
|
+
},
|
|
34
|
+
"rate_limit": {
|
|
35
|
+
"requests_per_minute": 60,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _deep_merge(base: dict, override: dict) -> dict:
|
|
41
|
+
result = base.copy()
|
|
42
|
+
for key, value in override.items():
|
|
43
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
44
|
+
result[key] = _deep_merge(result[key], value)
|
|
45
|
+
else:
|
|
46
|
+
result[key] = value
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_config(path: str = "config.yaml") -> dict:
|
|
51
|
+
config_path = Path(path)
|
|
52
|
+
if config_path.exists():
|
|
53
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
54
|
+
user_config = yaml.safe_load(f) or {}
|
|
55
|
+
return _deep_merge(DEFAULT_CONFIG, user_config)
|
|
56
|
+
return DEFAULT_CONFIG.copy()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def save_config(config: dict, path: str = "config.yaml"):
|
|
60
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
61
|
+
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
key_manager/core.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""High-level facade for programmatic usage."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from key_manager.config import load_config
|
|
8
|
+
from key_manager.storage import KeyStore
|
|
9
|
+
from key_manager.providers import PROVIDERS, get_display_name
|
|
10
|
+
from key_manager.detector import detect_provider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class KeyManager:
|
|
14
|
+
"""Convenient facade for managing API keys programmatically.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
km = KeyManager()
|
|
18
|
+
km.import_keys("keys.json")
|
|
19
|
+
results = km.check(provider="openai")
|
|
20
|
+
km.save()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
config_path: str = "config.yaml",
|
|
26
|
+
config: dict[str, Any] | None = None
|
|
27
|
+
):
|
|
28
|
+
"""Initialize KeyManager.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config_path: Path to config.yaml file
|
|
32
|
+
config: Direct config dict (overrides config_path)
|
|
33
|
+
"""
|
|
34
|
+
self.config = config or load_config(config_path)
|
|
35
|
+
self._store = KeyStore(
|
|
36
|
+
self.config["storage"]["keys_file"],
|
|
37
|
+
self.config
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def providers(self) -> dict[str, Any]:
|
|
42
|
+
"""Available provider registry."""
|
|
43
|
+
return PROVIDERS
|
|
44
|
+
|
|
45
|
+
def list_providers(self) -> list[str]:
|
|
46
|
+
"""List available provider names."""
|
|
47
|
+
return list(PROVIDERS.keys())
|
|
48
|
+
|
|
49
|
+
def get_provider(self, name: str):
|
|
50
|
+
"""Get a provider by name."""
|
|
51
|
+
return PROVIDERS.get(name)
|
|
52
|
+
|
|
53
|
+
def get_provider_display_name(self, name: str) -> str:
|
|
54
|
+
"""Get human-readable display name for a provider."""
|
|
55
|
+
return get_display_name(name)
|
|
56
|
+
|
|
57
|
+
def load_keys(self) -> dict[str, Any]:
|
|
58
|
+
"""Load keys from encrypted storage."""
|
|
59
|
+
return self._store.load()
|
|
60
|
+
|
|
61
|
+
def save_keys(self, data: dict[str, Any]) -> None:
|
|
62
|
+
"""Save keys to encrypted storage."""
|
|
63
|
+
self._store.save(data)
|
|
64
|
+
|
|
65
|
+
def detect_provider(self, key: str) -> list[str]:
|
|
66
|
+
"""Auto-detect provider from key prefix."""
|
|
67
|
+
return detect_provider(key)
|
|
68
|
+
|
|
69
|
+
def list_keys(
|
|
70
|
+
self,
|
|
71
|
+
provider: str | None = None,
|
|
72
|
+
status: str | None = None
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
"""List keys with optional filtering.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
provider: Filter by provider name
|
|
78
|
+
status: Filter by status (valid/invalid/error)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dict with keys matching filters
|
|
82
|
+
"""
|
|
83
|
+
data = self.load_keys()
|
|
84
|
+
result = {}
|
|
85
|
+
|
|
86
|
+
for key, info in data.get("keys", {}).items():
|
|
87
|
+
if provider and info.get("provider", "").lower() != provider.lower():
|
|
88
|
+
continue
|
|
89
|
+
if status and info.get("status") != status:
|
|
90
|
+
continue
|
|
91
|
+
result[key] = info
|
|
92
|
+
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
def get_stats(self) -> dict[str, Any]:
|
|
96
|
+
"""Get statistics about stored keys.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Dict with provider counts and status breakdown
|
|
100
|
+
"""
|
|
101
|
+
data = self.load_keys()
|
|
102
|
+
stats: dict[str, Any] = {
|
|
103
|
+
"total": 0,
|
|
104
|
+
"by_provider": {},
|
|
105
|
+
"by_status": {"valid": 0, "invalid": 0, "error": 0, "unknown": 0}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for key, info in data.get("keys", {}).items():
|
|
109
|
+
stats["total"] += 1
|
|
110
|
+
provider = info.get("provider", "unknown")
|
|
111
|
+
status = info.get("status", "unknown")
|
|
112
|
+
|
|
113
|
+
if provider not in stats["by_provider"]:
|
|
114
|
+
stats["by_provider"][provider] = 0
|
|
115
|
+
stats["by_provider"][provider] += 1
|
|
116
|
+
|
|
117
|
+
if status in stats["by_status"]:
|
|
118
|
+
stats["by_status"][status] += 1
|
|
119
|
+
|
|
120
|
+
return stats
|
|
121
|
+
|
|
122
|
+
async def check_key(
|
|
123
|
+
self,
|
|
124
|
+
key: str,
|
|
125
|
+
provider: str | None = None,
|
|
126
|
+
timeout: int = 30
|
|
127
|
+
) -> dict[str, Any]:
|
|
128
|
+
"""Check a single key.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
key: API key to check
|
|
132
|
+
provider: Provider name (auto-detected if not specified)
|
|
133
|
+
timeout: Request timeout in seconds
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dict with check results
|
|
137
|
+
"""
|
|
138
|
+
import httpx
|
|
139
|
+
from key_manager.providers import PROVIDERS
|
|
140
|
+
|
|
141
|
+
if not provider:
|
|
142
|
+
providers = self.detect_provider(key)
|
|
143
|
+
if not providers:
|
|
144
|
+
return {"valid": False, "error": "Could not detect provider"}
|
|
145
|
+
provider = providers[0]
|
|
146
|
+
|
|
147
|
+
provider_obj = PROVIDERS.get(provider)
|
|
148
|
+
if not provider_obj:
|
|
149
|
+
return {"valid": False, "error": f"Unknown provider: {provider}"}
|
|
150
|
+
|
|
151
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
152
|
+
result = await provider_obj.check(client, key)
|
|
153
|
+
return {
|
|
154
|
+
"valid": result.valid,
|
|
155
|
+
"provider": provider,
|
|
156
|
+
"status_code": result.status_code,
|
|
157
|
+
"latency_ms": result.latency_ms,
|
|
158
|
+
"error": result.error
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async def check_all(
|
|
162
|
+
self,
|
|
163
|
+
provider: str | None = None,
|
|
164
|
+
status: str | None = None,
|
|
165
|
+
concurrency: int = 100,
|
|
166
|
+
timeout: int = 30
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""Check all keys in storage.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
provider: Filter by provider
|
|
172
|
+
status: Filter by status
|
|
173
|
+
concurrency: Max concurrent checks
|
|
174
|
+
timeout: Request timeout
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Dict with check results
|
|
178
|
+
"""
|
|
179
|
+
from key_manager.checker import run_check
|
|
180
|
+
|
|
181
|
+
return await run_check(
|
|
182
|
+
keys_file=self.config["storage"]["keys_file"],
|
|
183
|
+
results_file=self.config["storage"]["check_results_file"],
|
|
184
|
+
logs_dir=self.config["storage"]["logs_dir"],
|
|
185
|
+
concurrency=concurrency,
|
|
186
|
+
timeout=timeout,
|
|
187
|
+
proxy=self.config.get("proxy") or None,
|
|
188
|
+
retry_failed=self.config["check"]["retry_failed"],
|
|
189
|
+
retry_count=self.config["check"]["retry_count"]
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
async def validate_keys(
|
|
193
|
+
self,
|
|
194
|
+
provider: str | None = None,
|
|
195
|
+
status: str | None = None,
|
|
196
|
+
concurrency: int = 100,
|
|
197
|
+
timeout: int = 30
|
|
198
|
+
) -> dict[str, Any]:
|
|
199
|
+
"""Validate all keys (alias for check_all)."""
|
|
200
|
+
return await self.check_all(
|
|
201
|
+
provider=provider,
|
|
202
|
+
status=status,
|
|
203
|
+
concurrency=concurrency,
|
|
204
|
+
timeout=timeout
|
|
205
|
+
)
|