|
7 | 7 | import subprocess |
8 | 8 | import urllib.error |
9 | 9 | import urllib.request |
| 10 | +from collections.abc import Callable |
10 | 11 | from pathlib import Path |
11 | 12 | from typing import Any |
12 | 13 |
|
13 | 14 | from dotenv import dotenv_values |
14 | 15 |
|
15 | 16 | from .models import PlanConfig, TargetConfig |
16 | 17 |
|
| 18 | +LogFn = Callable[[str], None] |
| 19 | + |
17 | 20 |
|
18 | 21 | def read_env_file(path: Path | None) -> dict[str, str]: |
19 | 22 | """读取 dotenv 环境变量文件。 |
@@ -47,6 +50,29 @@ def read_api_key(config: TargetConfig) -> str | None: |
47 | 50 | return read_env_file(config.env_file).get(config.api_key_env) |
48 | 51 |
|
49 | 52 |
|
| 53 | +def read_api_key_with_source(config: TargetConfig) -> tuple[str | None, str]: |
| 54 | + """读取目标服务 API key 及其来源。 |
| 55 | +
|
| 56 | + Args: |
| 57 | + config: 目标服务连接配置。 |
| 58 | +
|
| 59 | + Returns: |
| 60 | + `(API key, 来源描述)`;未配置或未找到时 API key 为 None。 |
| 61 | + """ |
| 62 | + if config.api_key_env is None: |
| 63 | + return None, "disabled" |
| 64 | + |
| 65 | + env_value = os.getenv(config.api_key_env) |
| 66 | + if env_value: |
| 67 | + return env_value, f"env:{config.api_key_env}" |
| 68 | + |
| 69 | + env_values = read_env_file(config.env_file) |
| 70 | + file_value = env_values.get(config.api_key_env) |
| 71 | + if file_value: |
| 72 | + return file_value, f"file:{format_path(config.env_file)}" |
| 73 | + return None, "missing" |
| 74 | + |
| 75 | + |
50 | 76 | def is_container_running(container_name: str) -> tuple[bool, str]: |
51 | 77 | """检查 Docker 容器是否处于 running 状态。 |
52 | 78 |
|
@@ -199,25 +225,65 @@ def call_litellm_completion(**kwargs: Any) -> None: |
199 | 225 | completion(**kwargs) |
200 | 226 |
|
201 | 227 |
|
202 | | -def ensure_target_ready(config: TargetConfig) -> tuple[bool, str, str | None]: |
| 228 | +def ensure_target_ready(config: TargetConfig, log_fn: LogFn | None = None) -> tuple[bool, str, str | None]: |
203 | 229 | """确认目标前置条件与 API 均可用。 |
204 | 230 |
|
205 | 231 | Args: |
206 | 232 | config: 目标服务连接配置。 |
| 233 | + log_fn: 可选日志函数,用于输出检查链路。 |
207 | 234 |
|
208 | 235 | Returns: |
209 | 236 | `(是否可用, 诊断信息, API key)`。 |
210 | 237 | """ |
211 | 238 | if config.container_name is not None: |
| 239 | + emit(log_fn, f"container check started container={config.container_name}") |
212 | 240 | container_running, container_message = is_container_running(config.container_name) |
213 | 241 | if not container_running: |
| 242 | + emit(log_fn, f"container check failed container={config.container_name} reason={container_message}") |
214 | 243 | return False, f"容器 {config.container_name} 未就绪: {container_message}", None |
| 244 | + emit(log_fn, f"container check passed container={config.container_name}") |
| 245 | + else: |
| 246 | + emit(log_fn, "container check skipped") |
215 | 247 |
|
216 | | - api_key = read_api_key(config) |
| 248 | + api_key, api_key_source = read_api_key_with_source(config) |
217 | 249 | if config.api_key_env is not None and api_key is None: |
| 250 | + emit(log_fn, f"api key check failed source={api_key_source} env={config.api_key_env}") |
218 | 251 | return False, f"未找到 {config.api_key_env}", None |
| 252 | + emit(log_fn, f"api key check passed source={api_key_source}") |
219 | 253 |
|
| 254 | + if config.health_path is not None: |
| 255 | + emit(log_fn, f"health check started url={join_url(config.base_url, config.health_path)}") |
220 | 256 | api_ready, api_message = is_target_api_ready(config, api_key) |
221 | 257 | if not api_ready: |
| 258 | + emit(log_fn, f"health check failed reason={api_message}") |
222 | 259 | return False, f"{config.name} API 未就绪: {api_message}", api_key |
223 | | - return True, f"{config.name} 已就绪", api_key |
| 260 | + emit(log_fn, f"health check passed result={api_message}") |
| 261 | + return True, f"{config.name} 已就绪 api_key_source={api_key_source}", api_key |
| 262 | + |
| 263 | + |
| 264 | +def emit(log_fn: LogFn | None, message: str) -> None: |
| 265 | + """按需输出目标检查日志。 |
| 266 | +
|
| 267 | + Args: |
| 268 | + log_fn: 可选日志函数。 |
| 269 | + message: 日志消息。 |
| 270 | +
|
| 271 | + Returns: |
| 272 | + 无返回值。 |
| 273 | + """ |
| 274 | + if log_fn is not None: |
| 275 | + log_fn(message) |
| 276 | + |
| 277 | + |
| 278 | +def format_path(path: Path | None) -> str: |
| 279 | + """格式化日志中的路径值。 |
| 280 | +
|
| 281 | + Args: |
| 282 | + path: 可选路径。 |
| 283 | +
|
| 284 | + Returns: |
| 285 | + 适合日志展示的路径文本。 |
| 286 | + """ |
| 287 | + if path is None: |
| 288 | + return "none" |
| 289 | + return str(path) |
0 commit comments