api-key-manager 5.0.1__tar.gz → 5.0.2__tar.gz
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-5.0.1 → api_key_manager-5.0.2}/PKG-INFO +4 -4
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/README.md +3 -3
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/api_key_manager.egg-info/PKG-INFO +4 -4
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/__init__.py +1 -1
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/config.py +23 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/logger.py +18 -6
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/storage.py +56 -6
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/_app.py +24 -11
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/middleware.py +16 -11
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/routes/balance.py +2 -2
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/routes/check.py +6 -6
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/routes/keys.py +8 -8
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/routes/misc.py +4 -4
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/routes/test.py +3 -3
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/pyproject.toml +1 -1
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_bug_fixes.py +36 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_logger.py +19 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_routes_misc.py +18 -10
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_storage.py +32 -4
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/LICENSE +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/api_key_manager.egg-info/SOURCES.txt +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/api_key_manager.egg-info/dependency_links.txt +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/api_key_manager.egg-info/entry_points.txt +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/api_key_manager.egg-info/requires.txt +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/api_key_manager.egg-info/top_level.txt +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/__main__.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/api_models.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/checker.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/cli.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/core.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/detector.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/errors.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/i18n.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/model_capabilities.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/parser.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/__init__.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/ai302.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/anthropic.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/baichuan.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/base.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/cerebras.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/cohere.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/config_providers.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/cstcloud.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/dashscope.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/dashscope_coding.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/deepseek.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/dmxapi.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/doubao.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/fireworks.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/google.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/grok.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/groq.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/huggingface.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/hyperbolic.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/infini.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/infini_coding.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/kimi.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/kimi_coding.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/longcat.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/mimo.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/mimo_plan.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/minimax.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/minimax_plan.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/mistral.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/models_registry.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/modelscope.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/nvidia.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/ocoolai.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/openai.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/opencode.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/opencode_zen.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/openrouter.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/perplexity.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/poe.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/ppio.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/replicate.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/siliconflow.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/stepfun.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/tencent_hunyuan.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/together.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/yi.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/zai.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/zhipu.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/providers/zhipu_coding.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/proxy.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/py.typed +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/ssrf.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/tester.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/url_override.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/validator.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/__init__.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/progress.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/routes/__init__.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/routes/models.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/routes/providers.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/routes/stats.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/web/utils.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/key_manager/webhook.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/setup.cfg +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_api_endpoints.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_base_check.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_checker.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_core.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_deepseek_balance.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_detector_unit.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_e2e.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_errors.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_i18n.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_openapi.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_parser.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_provider_detection.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_provider_refactoring.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_providers.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_proxy.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_routes_check.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_routes_models.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_routes_providers.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_routes_test.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_security.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_tester.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_token_limit_fast.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_v320_changes.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_validator.py +0 -0
- {api_key_manager-5.0.1 → api_key_manager-5.0.2}/tests/test_webhook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-key-manager
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.2
|
|
4
4
|
Summary: Batch manage API keys for 45+ AI providers with CLI and Web interfaces
|
|
5
5
|
Author: Townrain
|
|
6
6
|
License: MIT
|
|
@@ -62,14 +62,14 @@ Dynamic: license-file
|
|
|
62
62
|
- **API 文档** - Swagger UI 和 Redoc 自动文档
|
|
63
63
|
- **国际化** - 支持中英文错误信息
|
|
64
64
|
- **SDK 支持** - Python 和 TypeScript 客户端库
|
|
65
|
-
- **桌面应用** -
|
|
65
|
+
- **桌面应用** - 单 exe 便携安装,React 前端 + pywebview 桌面窗口
|
|
66
66
|
- **Webhook 通知** - 事件驱动的 Webhook 通知系统
|
|
67
67
|
|
|
68
68
|
## 快速开始
|
|
69
69
|
|
|
70
|
-
### 桌面应用(v5.0.
|
|
70
|
+
### 桌面应用(v5.0.2)
|
|
71
71
|
|
|
72
|
-
从 [Releases](https://github.com/Townrain/API-Key-Manager/releases)
|
|
72
|
+
从 [Releases](https://github.com/Townrain/API-Key-Manager/releases) 下载 `KeyHub-Setup.exe`,选择任意目录安装即可。首次启动自动生成配置文件,所有数据保持在安装目录内。
|
|
73
73
|
|
|
74
74
|
### CLI / Web
|
|
75
75
|
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
- **API 文档** - Swagger UI 和 Redoc 自动文档
|
|
23
23
|
- **国际化** - 支持中英文错误信息
|
|
24
24
|
- **SDK 支持** - Python 和 TypeScript 客户端库
|
|
25
|
-
- **桌面应用** -
|
|
25
|
+
- **桌面应用** - 单 exe 便携安装,React 前端 + pywebview 桌面窗口
|
|
26
26
|
- **Webhook 通知** - 事件驱动的 Webhook 通知系统
|
|
27
27
|
|
|
28
28
|
## 快速开始
|
|
29
29
|
|
|
30
|
-
### 桌面应用(v5.0.
|
|
30
|
+
### 桌面应用(v5.0.2)
|
|
31
31
|
|
|
32
|
-
从 [Releases](https://github.com/Townrain/API-Key-Manager/releases)
|
|
32
|
+
从 [Releases](https://github.com/Townrain/API-Key-Manager/releases) 下载 `KeyHub-Setup.exe`,选择任意目录安装即可。首次启动自动生成配置文件,所有数据保持在安装目录内。
|
|
33
33
|
|
|
34
34
|
### CLI / Web
|
|
35
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-key-manager
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.2
|
|
4
4
|
Summary: Batch manage API keys for 45+ AI providers with CLI and Web interfaces
|
|
5
5
|
Author: Townrain
|
|
6
6
|
License: MIT
|
|
@@ -62,14 +62,14 @@ Dynamic: license-file
|
|
|
62
62
|
- **API 文档** - Swagger UI 和 Redoc 自动文档
|
|
63
63
|
- **国际化** - 支持中英文错误信息
|
|
64
64
|
- **SDK 支持** - Python 和 TypeScript 客户端库
|
|
65
|
-
- **桌面应用** -
|
|
65
|
+
- **桌面应用** - 单 exe 便携安装,React 前端 + pywebview 桌面窗口
|
|
66
66
|
- **Webhook 通知** - 事件驱动的 Webhook 通知系统
|
|
67
67
|
|
|
68
68
|
## 快速开始
|
|
69
69
|
|
|
70
|
-
### 桌面应用(v5.0.
|
|
70
|
+
### 桌面应用(v5.0.2)
|
|
71
71
|
|
|
72
|
-
从 [Releases](https://github.com/Townrain/API-Key-Manager/releases)
|
|
72
|
+
从 [Releases](https://github.com/Townrain/API-Key-Manager/releases) 下载 `KeyHub-Setup.exe`,选择任意目录安装即可。首次启动自动生成配置文件,所有数据保持在安装目录内。
|
|
73
73
|
|
|
74
74
|
### CLI / Web
|
|
75
75
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import logging
|
|
3
|
+
import sys
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import yaml
|
|
@@ -77,8 +78,30 @@ def _validate_config(config: dict) -> dict:
|
|
|
77
78
|
return config
|
|
78
79
|
|
|
79
80
|
|
|
81
|
+
def _copy_example_config(config_path: Path):
|
|
82
|
+
"""Copy bundled config.yaml.example to config.yaml on first run."""
|
|
83
|
+
if getattr(sys, "frozen", False):
|
|
84
|
+
sample = Path(sys._MEIPASS) / "config.yaml.example"
|
|
85
|
+
else:
|
|
86
|
+
sample = Path("config.yaml.example")
|
|
87
|
+
if sample.exists():
|
|
88
|
+
try:
|
|
89
|
+
config_path.write_bytes(sample.read_bytes())
|
|
90
|
+
logger.info(f"Created default config at {config_path}")
|
|
91
|
+
except OSError:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
80
95
|
def load_config(path: str = "config.yaml") -> dict:
|
|
96
|
+
# PyInstaller exe: resolve path next to the exe, auto-create from example
|
|
97
|
+
if getattr(sys, "frozen", False):
|
|
98
|
+
exe_dir = Path(sys.argv[0]).parent
|
|
99
|
+
path = str(exe_dir / "config.yaml")
|
|
100
|
+
|
|
81
101
|
config_path = Path(path)
|
|
102
|
+
if not config_path.exists():
|
|
103
|
+
_copy_example_config(config_path)
|
|
104
|
+
|
|
82
105
|
if config_path.exists():
|
|
83
106
|
try:
|
|
84
107
|
with open(config_path, encoding="utf-8") as f:
|
|
@@ -4,11 +4,18 @@ from datetime import datetime
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
def _ensure_logs_dir(logs_dir: str = "./data/logs") -> Path:
|
|
8
|
+
"""Create and return the logs directory."""
|
|
9
|
+
d = Path(logs_dir)
|
|
10
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
11
|
+
return d
|
|
12
|
+
|
|
13
|
+
|
|
7
14
|
class KeyLogger:
|
|
8
15
|
"""Structured logger with date-based file rotation."""
|
|
9
16
|
|
|
10
17
|
def __init__(self, logs_dir: str, category: str):
|
|
11
|
-
self.logs_dir =
|
|
18
|
+
self.logs_dir = _ensure_logs_dir(logs_dir)
|
|
12
19
|
self.category = category
|
|
13
20
|
self.logger = logging.getLogger(f"keymanager.{category}")
|
|
14
21
|
self.logger.setLevel(logging.DEBUG)
|
|
@@ -16,7 +23,6 @@ class KeyLogger:
|
|
|
16
23
|
self._setup_file_handler()
|
|
17
24
|
|
|
18
25
|
def _setup_file_handler(self):
|
|
19
|
-
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
20
26
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
21
27
|
log_file = self.logs_dir / f"{self.category}_{date_str}.log"
|
|
22
28
|
|
|
@@ -44,8 +50,7 @@ class ProjectLogger:
|
|
|
44
50
|
"""Main project logger that logs all operations."""
|
|
45
51
|
|
|
46
52
|
def __init__(self, logs_dir: str = "./data/logs"):
|
|
47
|
-
self.logs_dir =
|
|
48
|
-
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
self.logs_dir = _ensure_logs_dir(logs_dir)
|
|
49
54
|
self._setup_loggers()
|
|
50
55
|
|
|
51
56
|
def _setup_loggers(self):
|
|
@@ -228,5 +233,12 @@ class ProjectLogger:
|
|
|
228
233
|
return {"success": True, "date": date, "deleted_lines": lines_count}
|
|
229
234
|
|
|
230
235
|
|
|
231
|
-
#
|
|
232
|
-
|
|
236
|
+
# Lazy singleton — defers directory creation to first get_project_logger() call
|
|
237
|
+
_project_logger: "ProjectLogger | None" = None
|
|
238
|
+
|
|
239
|
+
def get_project_logger() -> "ProjectLogger":
|
|
240
|
+
"""Return the singleton ProjectLogger, creating it on first access."""
|
|
241
|
+
global _project_logger
|
|
242
|
+
if _project_logger is None:
|
|
243
|
+
_project_logger = ProjectLogger()
|
|
244
|
+
return _project_logger
|
|
@@ -3,6 +3,7 @@ import json
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
import secrets
|
|
6
|
+
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
from cryptography.hazmat.primitives import hashes
|
|
@@ -14,6 +15,13 @@ from key_manager.errors import ErrorCode, StorageError
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
def _resolve_config_path() -> Path:
|
|
19
|
+
"""Resolve config.yaml path — next to exe when frozen, CWD otherwise."""
|
|
20
|
+
if getattr(sys, "frozen", False):
|
|
21
|
+
return Path(sys.argv[0]).parent / "config.yaml"
|
|
22
|
+
return Path("config.yaml")
|
|
23
|
+
|
|
24
|
+
|
|
17
25
|
_ITERATIONS = 600_000 # OWASP 2023 recommendation for PBKDF2-HMAC-SHA256
|
|
18
26
|
_LEGACY_SALT = b"key-manager-aes256gcm-salt-v1" # For backward compatibility
|
|
19
27
|
_API_TOKEN_SALT = b"key-manager-api-auth-token-v1" # Domain-specific salt for API token
|
|
@@ -51,6 +59,20 @@ def clear_key_cache() -> None:
|
|
|
51
59
|
_key_cache.clear()
|
|
52
60
|
|
|
53
61
|
|
|
62
|
+
def clear_passphrase_cache() -> None:
|
|
63
|
+
"""Clear the module-level passphrase cache.
|
|
64
|
+
|
|
65
|
+
Call this in test teardown to prevent cross-test pollution.
|
|
66
|
+
"""
|
|
67
|
+
global _passphrase_cache
|
|
68
|
+
_passphrase_cache = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def clear_all_caches() -> None:
|
|
72
|
+
"""Clear all module-level caches (keys + passphrase)."""
|
|
73
|
+
clear_key_cache()
|
|
74
|
+
clear_passphrase_cache()
|
|
75
|
+
|
|
54
76
|
def derive_api_token(config: dict | None = None, salt: bytes = None) -> str:
|
|
55
77
|
"""Derive an API authentication token from the encryption passphrase.
|
|
56
78
|
|
|
@@ -80,27 +102,53 @@ def derive_api_token(config: dict | None = None, salt: bytes = None) -> str:
|
|
|
80
102
|
_key_cache[cache_key] = token_bytes
|
|
81
103
|
return token_bytes.hex()
|
|
82
104
|
|
|
105
|
+
_passphrase_cache: str | None = None
|
|
106
|
+
|
|
83
107
|
def _get_passphrase(config: dict | None = None) -> str:
|
|
84
|
-
"""Get encryption passphrase from
|
|
108
|
+
"""Get encryption passphrase from cache, env var, config, or auto-generate.
|
|
109
|
+
|
|
110
|
+
Uses a module-level cache so all callers get the same passphrase,
|
|
111
|
+
even when config dicts are loaded at different times.
|
|
112
|
+
"""
|
|
113
|
+
global _passphrase_cache
|
|
114
|
+
if _passphrase_cache is not None:
|
|
115
|
+
return _passphrase_cache
|
|
116
|
+
|
|
85
117
|
# 1. Check environment variable
|
|
86
118
|
secret = os.environ.get("KEY_MANAGER_SECRET")
|
|
87
119
|
if secret:
|
|
120
|
+
_passphrase_cache = secret
|
|
88
121
|
return secret
|
|
89
122
|
|
|
90
|
-
# 2. Check config
|
|
123
|
+
# 2. Check in-memory config dict (fast path)
|
|
91
124
|
if config and config.get("encryption", {}).get("passphrase"):
|
|
92
|
-
|
|
125
|
+
_passphrase_cache = config["encryption"]["passphrase"]
|
|
126
|
+
return _passphrase_cache
|
|
93
127
|
|
|
94
|
-
# 3.
|
|
128
|
+
# 3. Read from config file on disk (catches passphrases saved by another caller)
|
|
129
|
+
try:
|
|
130
|
+
import yaml
|
|
131
|
+
config_path = _resolve_config_path()
|
|
132
|
+
if config_path.exists():
|
|
133
|
+
disk_cfg = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
|
134
|
+
passphrase = disk_cfg.get("encryption", {}).get("passphrase")
|
|
135
|
+
if passphrase:
|
|
136
|
+
_passphrase_cache = passphrase
|
|
137
|
+
return passphrase
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
# 4. Auto-generate and save
|
|
95
142
|
passphrase = secrets.token_urlsafe(32)
|
|
96
143
|
_save_passphrase_to_config(passphrase)
|
|
144
|
+
_passphrase_cache = passphrase
|
|
97
145
|
logger.info("Auto-generated encryption passphrase and saved to config.yaml")
|
|
98
146
|
return passphrase
|
|
99
147
|
|
|
100
148
|
|
|
101
149
|
def _save_passphrase_to_config(passphrase: str) -> None:
|
|
102
150
|
"""Append encryption passphrase to config.yaml without rewriting existing content."""
|
|
103
|
-
config_path =
|
|
151
|
+
config_path = _resolve_config_path()
|
|
104
152
|
|
|
105
153
|
try:
|
|
106
154
|
existing = ""
|
|
@@ -178,7 +226,7 @@ class KeyStore:
|
|
|
178
226
|
encrypted = self.config.get("storage", {}).get("encrypted", True) is True
|
|
179
227
|
if not encrypted:
|
|
180
228
|
logger.warning("rotate_key called but storage.encrypted=false — passphrase not used")
|
|
181
|
-
clear_key_cache()
|
|
229
|
+
clear_key_cache()
|
|
182
230
|
if not self.path.exists():
|
|
183
231
|
raise StorageError(code=ErrorCode.STORAGE_READ_ERROR, message=f"File not found: {self.path}")
|
|
184
232
|
raw = self.path.read_text(encoding="utf-8")
|
|
@@ -190,9 +238,11 @@ class KeyStore:
|
|
|
190
238
|
data = self._decrypt(envelope)
|
|
191
239
|
else:
|
|
192
240
|
data = envelope
|
|
241
|
+
clear_passphrase_cache() # clear old cache so _encrypt uses new passphrase
|
|
193
242
|
self.config.setdefault("encryption", {})["passphrase"] = new_passphrase
|
|
194
243
|
os.environ["KEY_MANAGER_SECRET"] = new_passphrase
|
|
195
244
|
self.save(data)
|
|
245
|
+
clear_passphrase_cache() # clean up: don't leak passphrase to caller's env
|
|
196
246
|
return data
|
|
197
247
|
|
|
198
248
|
def _encrypt(self, data: dict) -> dict:
|
|
@@ -14,7 +14,7 @@ from fastapi.templating import Jinja2Templates
|
|
|
14
14
|
from key_manager.config import load_config
|
|
15
15
|
from key_manager.detector import detect_provider # noqa: F401
|
|
16
16
|
from key_manager.errors import StorageError
|
|
17
|
-
from key_manager.logger import
|
|
17
|
+
from key_manager.logger import get_project_logger
|
|
18
18
|
from key_manager.parser import (
|
|
19
19
|
import_keys, # noqa: F401
|
|
20
20
|
validate_import_path, # noqa: F401
|
|
@@ -58,7 +58,7 @@ app = FastAPI(
|
|
|
58
58
|
description="Manage and validate API keys for 45+ AI providers. "
|
|
59
59
|
"Import, check validity, test token limits and concurrency, "
|
|
60
60
|
"query balances, and export working keys.",
|
|
61
|
-
version="5.0.
|
|
61
|
+
version="5.0.2",
|
|
62
62
|
openapi_tags=[
|
|
63
63
|
{"name": "Keys", "description": "Key management operations"},
|
|
64
64
|
{"name": "Check", "description": "Key validity checking"},
|
|
@@ -76,7 +76,7 @@ app = FastAPI(
|
|
|
76
76
|
|
|
77
77
|
app.add_middleware(
|
|
78
78
|
CORSMiddleware,
|
|
79
|
-
allow_origins=["http://localhost:18001", "http://127.0.0.1:18001", "http://localhost:3000", "http://127.0.0.1:3000"],
|
|
79
|
+
allow_origins=["http://localhost:18001", "http://127.0.0.1:18001", "http://localhost:3000", "http://127.0.0.1:3000", "null"],
|
|
80
80
|
allow_credentials=False, # Must be False when allow_origins is "*" (CORS spec)
|
|
81
81
|
allow_methods=["*"],
|
|
82
82
|
allow_headers=["*"],
|
|
@@ -110,7 +110,7 @@ def _load_keys_data(config_override: dict | None = None) -> dict:
|
|
|
110
110
|
except StorageError:
|
|
111
111
|
raise
|
|
112
112
|
except Exception as e:
|
|
113
|
-
|
|
113
|
+
get_project_logger().main_logger.warning(f"Failed to load/decrypt keys data: {e}")
|
|
114
114
|
# Return empty data structure for unexpected errors (e.g., corrupt JSON)
|
|
115
115
|
return {"keys": {}}
|
|
116
116
|
|
|
@@ -130,9 +130,20 @@ setup_error_handlers(app)
|
|
|
130
130
|
# WEB UI
|
|
131
131
|
# ---
|
|
132
132
|
|
|
133
|
+
_TAURI_DIR = Path(__file__).resolve().parent.parent.parent / "static_tauri"
|
|
133
134
|
_TEMPLATES_DIR = Path(__file__).resolve().parent.parent.parent / "templates"
|
|
134
135
|
_TEMPLATES_DIR_ALT = Path(__file__).resolve().parent.parent / "templates"
|
|
135
136
|
|
|
137
|
+
# Tauri React SPA: mount assets at /assets
|
|
138
|
+
if (_TAURI_DIR / "assets").is_dir():
|
|
139
|
+
app.mount("/assets", StaticFiles(directory=str(_TAURI_DIR / "assets")), name="tauri_assets")
|
|
140
|
+
|
|
141
|
+
# Legacy static files
|
|
142
|
+
_STATIC_DIR = Path(__file__).resolve().parent.parent.parent / "static"
|
|
143
|
+
if _STATIC_DIR.is_dir():
|
|
144
|
+
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
|
|
145
|
+
|
|
146
|
+
# Jinja2 templates (legacy fallback)
|
|
136
147
|
if _TEMPLATES_DIR.exists():
|
|
137
148
|
templates = Jinja2Templates(directory=str(_TEMPLATES_DIR))
|
|
138
149
|
elif _TEMPLATES_DIR_ALT.exists():
|
|
@@ -140,16 +151,10 @@ elif _TEMPLATES_DIR_ALT.exists():
|
|
|
140
151
|
else:
|
|
141
152
|
templates = None
|
|
142
153
|
|
|
143
|
-
# Static files (CSS, JS extracted from inline)
|
|
144
|
-
_STATIC_DIR = Path(__file__).resolve().parent.parent.parent / "static"
|
|
145
|
-
if _STATIC_DIR.is_dir():
|
|
146
|
-
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
|
|
147
|
-
|
|
148
154
|
|
|
149
155
|
@app.get("/", include_in_schema=False)
|
|
150
156
|
async def web_ui(request: Request):
|
|
151
157
|
"""Serve the web UI with API token injected."""
|
|
152
|
-
# Derive API token for frontend injection
|
|
153
158
|
api_token = ""
|
|
154
159
|
try:
|
|
155
160
|
from key_manager.storage import derive_api_token
|
|
@@ -157,13 +162,21 @@ async def web_ui(request: Request):
|
|
|
157
162
|
except Exception:
|
|
158
163
|
pass
|
|
159
164
|
|
|
165
|
+
# Tauri React SPA (preferred)
|
|
166
|
+
tauri_index = _TAURI_DIR / "index.html"
|
|
167
|
+
if tauri_index.exists():
|
|
168
|
+
html = tauri_index.read_text(encoding="utf-8")
|
|
169
|
+
html = html.replace("<title>tauri-compare</title>", "<title>KeyHub</title>")
|
|
170
|
+
html = html.replace("</head>", f"<script>window.__API_TOKEN__ = '{api_token}';</script></head>")
|
|
171
|
+
return HTMLResponse(html)
|
|
172
|
+
|
|
173
|
+
# Legacy Jinja2 template fallback
|
|
160
174
|
if templates and (_TEMPLATES_DIR / "index.html").exists():
|
|
161
175
|
return templates.TemplateResponse("index.html", {"request": request, "api_token": api_token})
|
|
162
176
|
if templates and (_TEMPLATES_DIR_ALT / "index.html").exists():
|
|
163
177
|
return templates.TemplateResponse("index.html", {"request": request, "api_token": api_token})
|
|
164
178
|
return HTMLResponse("<html><body><h1>API Key Manager</h1><p>Web UI not found.</p></body></html>")
|
|
165
179
|
|
|
166
|
-
|
|
167
180
|
# ---
|
|
168
181
|
# Route modules (extracted to key_manager/web/routes/)
|
|
169
182
|
# ---
|
|
@@ -94,7 +94,7 @@ async def rate_limit_middleware(request: Request, call_next):
|
|
|
94
94
|
# Authentication
|
|
95
95
|
# ---------------------------------------------------------------------------
|
|
96
96
|
|
|
97
|
-
_AUTH_WHITELIST = {"/", "/docs", "/redoc", "/openapi.json", "/static/"}
|
|
97
|
+
_AUTH_WHITELIST = {"/", "/docs", "/redoc", "/openapi.json", "/static/", "/assets/"}
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
async def auth_middleware(request: Request, call_next):
|
|
@@ -114,19 +114,11 @@ async def auth_middleware(request: Request, call_next):
|
|
|
114
114
|
pass
|
|
115
115
|
|
|
116
116
|
if not api_key:
|
|
117
|
-
# Log warning once at startup (not on every request)
|
|
118
|
-
app = request.app
|
|
119
|
-
if not hasattr(app, '_auth_warning_logged'):
|
|
120
|
-
logger.warning(
|
|
121
|
-
"Authentication is disabled. Set KEY_MANAGER_API_KEY env var or "
|
|
122
|
-
"auth.api_key in config.yaml to secure your API."
|
|
123
|
-
)
|
|
124
|
-
app._auth_warning_logged = True
|
|
125
117
|
return await call_next(request)
|
|
126
118
|
|
|
127
119
|
# Check whitelist (including static files)
|
|
128
120
|
path = request.url.path
|
|
129
|
-
if path in _AUTH_WHITELIST or path.startswith("/static/"):
|
|
121
|
+
if path in _AUTH_WHITELIST or path.startswith("/static/") or path.startswith("/assets/"):
|
|
130
122
|
return await call_next(request)
|
|
131
123
|
|
|
132
124
|
auth_header = request.headers.get("authorization", "")
|
|
@@ -139,7 +131,6 @@ async def auth_middleware(request: Request, call_next):
|
|
|
139
131
|
)
|
|
140
132
|
return JSONResponse(status_code=401, content=response.model_dump())
|
|
141
133
|
|
|
142
|
-
|
|
143
134
|
# ---------------------------------------------------------------------------
|
|
144
135
|
# Internationalization
|
|
145
136
|
# ---------------------------------------------------------------------------
|
|
@@ -209,6 +200,20 @@ def setup_middleware(app: FastAPI, config: dict) -> None:
|
|
|
209
200
|
|
|
210
201
|
def setup_error_handlers(app: FastAPI) -> None:
|
|
211
202
|
"""Register all error handlers on the given FastAPI app."""
|
|
203
|
+
import traceback
|
|
204
|
+
|
|
205
|
+
@app.exception_handler(Exception)
|
|
206
|
+
async def unhandled_exception_handler(request: Request, exc: Exception):
|
|
207
|
+
logger.error(f"Unhandled error on {request.method} {request.url.path}: {exc}")
|
|
208
|
+
logger.error(traceback.format_exc())
|
|
209
|
+
return JSONResponse(
|
|
210
|
+
status_code=500,
|
|
211
|
+
content=ErrorResponse(
|
|
212
|
+
error=ErrorCode.SYSTEM_INTERNAL_ERROR,
|
|
213
|
+
message=f"{type(exc).__name__}: {exc}",
|
|
214
|
+
).model_dump(),
|
|
215
|
+
)
|
|
216
|
+
|
|
212
217
|
app.exception_handler(RequestValidationError)(pydantic_validation_error_handler)
|
|
213
218
|
app.exception_handler(KeyManagerError)(key_manager_error_handler)
|
|
214
219
|
app.exception_handler(ValidationError)(validation_error_handler)
|
|
@@ -8,7 +8,7 @@ import key_manager.web._app as _app_mod
|
|
|
8
8
|
from key_manager.api_models import BalanceRequest, BalanceResponse
|
|
9
9
|
from key_manager.errors import ErrorCode, ValidationError
|
|
10
10
|
from key_manager.i18n import t
|
|
11
|
-
from key_manager.logger import
|
|
11
|
+
from key_manager.logger import get_project_logger
|
|
12
12
|
from key_manager.parser import mask_key
|
|
13
13
|
from key_manager.proxy import get_proxy
|
|
14
14
|
from key_manager.ssrf import get_allowed_domains, validate_custom_base_url
|
|
@@ -79,7 +79,7 @@ async def api_balance(body: BalanceRequest):
|
|
|
79
79
|
else:
|
|
80
80
|
error = "Provider does not support balance queries"
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
get_project_logger().log_web_action("balance", f"{mask_key(key)} {provider_name}: {balance_value}")
|
|
83
83
|
|
|
84
84
|
return BalanceResponse(
|
|
85
85
|
provider=provider_name,
|
|
@@ -19,7 +19,7 @@ from key_manager.api_models import (
|
|
|
19
19
|
from key_manager.detector import detect_by_prefix
|
|
20
20
|
from key_manager.errors import ErrorCode, ValidationError
|
|
21
21
|
from key_manager.i18n import t
|
|
22
|
-
from key_manager.logger import
|
|
22
|
+
from key_manager.logger import get_project_logger
|
|
23
23
|
from key_manager.parser import mask_key
|
|
24
24
|
from key_manager.providers import get_display_name
|
|
25
25
|
from key_manager.providers.base import CheckResult, simplify_error
|
|
@@ -104,7 +104,7 @@ async def api_check(
|
|
|
104
104
|
status_filter=s or None,
|
|
105
105
|
progress_callback=_make_progress_callback(),
|
|
106
106
|
)
|
|
107
|
-
|
|
107
|
+
get_project_logger().log_web_action("check_all", f"total={results.get('total', 0)}")
|
|
108
108
|
|
|
109
109
|
# Dispatch webhook
|
|
110
110
|
task = asyncio.create_task(
|
|
@@ -215,7 +215,7 @@ async def api_check_single(body: CheckSingleRequest):
|
|
|
215
215
|
|
|
216
216
|
status_str = "valid" if result.valid else ("invalid" if result.status_code in (401, 403) else "error")
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
get_project_logger().log_web_action("check_single", f"{mask_key(key)} {provider_name}: {status_str}")
|
|
219
219
|
|
|
220
220
|
# Simplify error message for readability
|
|
221
221
|
simplified_error = simplify_error(result.error, result.status_code) if result.error else None
|
|
@@ -242,7 +242,7 @@ async def api_check_single(body: CheckSingleRequest):
|
|
|
242
242
|
"created_at": timestamp,
|
|
243
243
|
}
|
|
244
244
|
_app_mod._save_keys_data(data)
|
|
245
|
-
|
|
245
|
+
get_project_logger().log_web_action("auto_import", f"{mask_key(key)} from check")
|
|
246
246
|
else:
|
|
247
247
|
# Update existing key status
|
|
248
248
|
keys_dict[key]["status"] = status_str
|
|
@@ -253,7 +253,7 @@ async def api_check_single(body: CheckSingleRequest):
|
|
|
253
253
|
keys_dict[key]["checks"].append({"status": status_str, "provider": provider_name, "timestamp": timestamp})
|
|
254
254
|
_app_mod._save_keys_data(data)
|
|
255
255
|
except Exception as e:
|
|
256
|
-
|
|
256
|
+
get_project_logger().main_logger.warning(f"Failed to auto-save checked key: {e}")
|
|
257
257
|
|
|
258
258
|
return CheckSingleResponse(
|
|
259
259
|
key_masked=mask_key(key),
|
|
@@ -352,6 +352,6 @@ async def api_check_batch(body: CheckBatchRequest):
|
|
|
352
352
|
summary.error += 1
|
|
353
353
|
|
|
354
354
|
summary.total = len(batch_results)
|
|
355
|
-
|
|
355
|
+
get_project_logger().log_web_action("check_batch", f"total={summary.total}, valid={summary.valid}")
|
|
356
356
|
|
|
357
357
|
return CheckBatchResponse(results=results, summary=summary)
|
|
@@ -19,7 +19,7 @@ from key_manager.api_models import (
|
|
|
19
19
|
)
|
|
20
20
|
from key_manager.errors import ErrorCode, ErrorResponse, ValidationError
|
|
21
21
|
from key_manager.i18n import t
|
|
22
|
-
from key_manager.logger import
|
|
22
|
+
from key_manager.logger import get_project_logger
|
|
23
23
|
from key_manager.parser import mask_key
|
|
24
24
|
|
|
25
25
|
router = APIRouter(tags=["Keys"])
|
|
@@ -62,7 +62,7 @@ async def api_import(body: ImportRequest):
|
|
|
62
62
|
}
|
|
63
63
|
new += 1
|
|
64
64
|
_app_mod._save_keys_data(data)
|
|
65
|
-
|
|
65
|
+
get_project_logger().log_web_action("import", f"batch: {new} new, {dupes} dupes")
|
|
66
66
|
return ImportResponse(new=new, duplicates=dupes, errors=errors)
|
|
67
67
|
|
|
68
68
|
allowed_dirs = _app_mod.config.get("scan", {}).get("directories", ["./data/input"])
|
|
@@ -80,7 +80,7 @@ async def api_import(body: ImportRequest):
|
|
|
80
80
|
batch=None,
|
|
81
81
|
keys_file=_app_mod.config["storage"]["keys_file"],
|
|
82
82
|
)
|
|
83
|
-
|
|
83
|
+
get_project_logger().log_web_action("import", f"{body.file or body.directory}: {new} new, {dupes} dupes")
|
|
84
84
|
return ImportResponse(new=new, duplicates=dupes, errors=errors)
|
|
85
85
|
|
|
86
86
|
|
|
@@ -143,7 +143,7 @@ async def api_import_upload(request: Request):
|
|
|
143
143
|
except Exception:
|
|
144
144
|
pass
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
get_project_logger().log_web_action("import_upload", f"{filename}: {new} new, {dupes} dupes")
|
|
147
147
|
return ImportResponse(new=new, duplicates=dupes, errors=errors)
|
|
148
148
|
|
|
149
149
|
|
|
@@ -225,7 +225,7 @@ async def api_export_keys(
|
|
|
225
225
|
max_concurrency=tests.get("max_concurrency"),
|
|
226
226
|
))
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
get_project_logger().log_web_action("export", f"{len(exported)} keys")
|
|
229
229
|
return KeyExportResponse(keys=exported, total=len(exported))
|
|
230
230
|
|
|
231
231
|
|
|
@@ -240,7 +240,7 @@ async def api_clear_keys():
|
|
|
240
240
|
cleared = len(data.get("keys", {}))
|
|
241
241
|
data["keys"] = {}
|
|
242
242
|
_app_mod._save_keys_data(data)
|
|
243
|
-
|
|
243
|
+
get_project_logger().log_web_action("clear", f"{cleared} keys removed")
|
|
244
244
|
return {"cleared": cleared}
|
|
245
245
|
|
|
246
246
|
|
|
@@ -268,7 +268,7 @@ async def api_get_full_key(request: Request):
|
|
|
268
268
|
# Find the full key by matching key_masked
|
|
269
269
|
for full_key, info in keys_dict.items():
|
|
270
270
|
if info.get("key_masked") == key_masked:
|
|
271
|
-
|
|
271
|
+
get_project_logger().log_web_action("get_full_key", key_masked)
|
|
272
272
|
return {"key": full_key}
|
|
273
273
|
|
|
274
274
|
raise ValidationError(
|
|
@@ -308,7 +308,7 @@ async def api_delete_key(request: Request):
|
|
|
308
308
|
if key_to_delete:
|
|
309
309
|
del keys_dict[key_to_delete]
|
|
310
310
|
_app_mod._save_keys_data(data)
|
|
311
|
-
|
|
311
|
+
get_project_logger().log_web_action("delete", key_masked)
|
|
312
312
|
return {"deleted": 1, "key_masked": key_masked}
|
|
313
313
|
|
|
314
314
|
raise ValidationError(
|
|
@@ -15,7 +15,7 @@ from key_manager.api_models import (
|
|
|
15
15
|
ProxyResponse,
|
|
16
16
|
)
|
|
17
17
|
from key_manager.errors import ErrorCode, ValidationError
|
|
18
|
-
from key_manager.logger import
|
|
18
|
+
from key_manager.logger import get_project_logger
|
|
19
19
|
from key_manager.proxy import get_proxy
|
|
20
20
|
from key_manager.web.progress import _progress_tracker, _sse_progress_event_generator
|
|
21
21
|
from key_manager.webhook import webhook_manager
|
|
@@ -53,14 +53,14 @@ async def api_proxy():
|
|
|
53
53
|
@router.get("/api/logs", tags=["Logs"], response_model=LogsResponse)
|
|
54
54
|
async def api_logs():
|
|
55
55
|
"""Get recent log entries."""
|
|
56
|
-
logs =
|
|
56
|
+
logs = get_project_logger().get_recent_logs()
|
|
57
57
|
return LogsResponse(logs=logs) # Return strings directly for frontend compatibility
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
@router.get("/api/logs/operations", tags=["Logs"], response_model=OperationsResponse)
|
|
61
61
|
async def api_logs_operations():
|
|
62
62
|
"""Get recent operation log entries."""
|
|
63
|
-
operations =
|
|
63
|
+
operations = get_project_logger().get_operations_log()
|
|
64
64
|
return OperationsResponse(operations=[OperationEntry(**entry) for entry in operations])
|
|
65
65
|
|
|
66
66
|
|
|
@@ -74,7 +74,7 @@ async def api_logs_clear(date: str = None):
|
|
|
74
74
|
Returns:
|
|
75
75
|
dict with success status and details
|
|
76
76
|
"""
|
|
77
|
-
result =
|
|
77
|
+
result = get_project_logger().clear_main_log(date)
|
|
78
78
|
return result
|
|
79
79
|
|
|
80
80
|
|
|
@@ -10,7 +10,7 @@ import key_manager.web._app as _app_mod
|
|
|
10
10
|
from key_manager.api_models import TestSingleRequest, TestSingleResponse
|
|
11
11
|
from key_manager.errors import ErrorCode, ValidationError
|
|
12
12
|
from key_manager.i18n import t
|
|
13
|
-
from key_manager.logger import
|
|
13
|
+
from key_manager.logger import get_project_logger
|
|
14
14
|
from key_manager.parser import mask_key
|
|
15
15
|
from key_manager.proxy import get_proxy
|
|
16
16
|
from key_manager.tester import run_test
|
|
@@ -40,7 +40,7 @@ async def api_test():
|
|
|
40
40
|
progress_callback=_make_progress_callback(),
|
|
41
41
|
)
|
|
42
42
|
_progress_tracker.done("done", results)
|
|
43
|
-
|
|
43
|
+
get_project_logger().log_web_action("test_all", f"tested={results.get('total_tested', 0)}")
|
|
44
44
|
await webhook_manager.dispatch(
|
|
45
45
|
WebhookEvent.BATCH_TEST_COMPLETED,
|
|
46
46
|
{"total_tested": results.get("total_tested", 0)},
|
|
@@ -128,7 +128,7 @@ async def api_test_single(body: TestSingleRequest):
|
|
|
128
128
|
except Exception:
|
|
129
129
|
pass
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
get_project_logger().log_web_action(
|
|
132
132
|
"test_single",
|
|
133
133
|
f"{mask_key(key)} {provider_name}: tokens={max_tokens}, concurrency={max_concurrency}",
|
|
134
134
|
)
|