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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. api_key_manager-2.1.0.dist-info/METADATA +709 -0
  2. api_key_manager-2.1.0.dist-info/RECORD +73 -0
  3. api_key_manager-2.1.0.dist-info/WHEEL +5 -0
  4. api_key_manager-2.1.0.dist-info/entry_points.txt +2 -0
  5. api_key_manager-2.1.0.dist-info/top_level.txt +1 -0
  6. key_manager/__init__.py +16 -0
  7. key_manager/__main__.py +5 -0
  8. key_manager/api_models.py +358 -0
  9. key_manager/checker.py +51 -0
  10. key_manager/cli.py +270 -0
  11. key_manager/config.py +61 -0
  12. key_manager/core.py +205 -0
  13. key_manager/detector.py +335 -0
  14. key_manager/errors.py +179 -0
  15. key_manager/i18n.py +142 -0
  16. key_manager/logger.py +207 -0
  17. key_manager/model_capabilities.py +412 -0
  18. key_manager/parser.py +153 -0
  19. key_manager/providers/__init__.py +283 -0
  20. key_manager/providers/ai302.py +109 -0
  21. key_manager/providers/anthropic.py +109 -0
  22. key_manager/providers/baichuan.py +97 -0
  23. key_manager/providers/base.py +312 -0
  24. key_manager/providers/cerebras.py +109 -0
  25. key_manager/providers/cohere.py +90 -0
  26. key_manager/providers/cstcloud.py +122 -0
  27. key_manager/providers/dashscope.py +120 -0
  28. key_manager/providers/dashscope_coding.py +122 -0
  29. key_manager/providers/deepseek.py +166 -0
  30. key_manager/providers/dmxapi.py +109 -0
  31. key_manager/providers/doubao.py +109 -0
  32. key_manager/providers/fireworks.py +109 -0
  33. key_manager/providers/google.py +99 -0
  34. key_manager/providers/grok.py +109 -0
  35. key_manager/providers/groq.py +109 -0
  36. key_manager/providers/huggingface.py +54 -0
  37. key_manager/providers/hyperbolic.py +109 -0
  38. key_manager/providers/infini.py +135 -0
  39. key_manager/providers/infini_coding.py +124 -0
  40. key_manager/providers/kimi.py +121 -0
  41. key_manager/providers/kimi_coding.py +124 -0
  42. key_manager/providers/longcat.py +123 -0
  43. key_manager/providers/mimo.py +109 -0
  44. key_manager/providers/mimo_plan.py +140 -0
  45. key_manager/providers/minimax.py +97 -0
  46. key_manager/providers/minimax_plan.py +122 -0
  47. key_manager/providers/mistral.py +109 -0
  48. key_manager/providers/models_registry.py +2901 -0
  49. key_manager/providers/modelscope.py +134 -0
  50. key_manager/providers/nvidia.py +109 -0
  51. key_manager/providers/ocoolai.py +109 -0
  52. key_manager/providers/openai.py +140 -0
  53. key_manager/providers/openrouter.py +119 -0
  54. key_manager/providers/perplexity.py +109 -0
  55. key_manager/providers/poe.py +109 -0
  56. key_manager/providers/ppio.py +109 -0
  57. key_manager/providers/replicate.py +54 -0
  58. key_manager/providers/siliconflow.py +121 -0
  59. key_manager/providers/stepfun.py +132 -0
  60. key_manager/providers/tencent_hunyuan.py +122 -0
  61. key_manager/providers/together.py +134 -0
  62. key_manager/providers/yi.py +97 -0
  63. key_manager/providers/zai.py +109 -0
  64. key_manager/providers/zhipu.py +127 -0
  65. key_manager/providers/zhipu_coding.py +124 -0
  66. key_manager/proxy.py +70 -0
  67. key_manager/ssrf.py +68 -0
  68. key_manager/storage.py +134 -0
  69. key_manager/tester.py +137 -0
  70. key_manager/url_override.py +5 -0
  71. key_manager/validator.py +185 -0
  72. key_manager/web.py +1512 -0
  73. key_manager/webhook.py +257 -0
key_manager/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
+ )