"""真实 LLM 服务的可用性判定(供真实 LLM 集成测试共用)。 真实 LLM 集成测试有两个外部前提:配置了 `LLM_API_KEY`、且服务端可用。 两者都属于**外部环境状态**(密钥、余额、配额、网络),不是被测代码的行为; 按测试规则,这类外部状态缺失时应跳过而不是把缺陷计到代码头上。 本模块把该判定收敛到一处,避免各测试各自实现、语义漂移: - `require_llm_credentials()`:无 Key 时直接跳过; - `skip_on_service_unavailable()`:把鉴权/余额/限流类 HTTP 错误(401/402/403/429) 转成跳过(附带原因),其余错误(如 5xx、网络异常、返回结构错误)照常失败, 以保证真实回归仍能被发现。 """ from __future__ import annotations import os import urllib.error from contextlib import contextmanager import pytest # 视为"服务端不可用/账号不可用"的 HTTP 状态码: # 401 未授权、402 需要付费(余额/额度耗尽)、403 禁止访问、429 限流。 _UNAVAILABLE_CODES = frozenset({401, 402, 403, 429}) def require_llm_credentials() -> None: """未配置 LLM_API_KEY 时跳过真实 LLM 集成测试。""" if not os.getenv("LLM_API_KEY"): pytest.skip("未配置 LLM_API_KEY,跳过真实 LLM 集成测试") @contextmanager def skip_on_service_unavailable(): """执行真实 LLM 调用;鉴权/余额/限流类错误转为跳过,其余错误向上抛出。 这样既不会因账户余额或临时限流把测试套件判红(外部状态问题), 也不会掩盖真正的回归(返回结构错误、代码异常仍会失败)。 """ try: yield except urllib.error.HTTPError as exc: if exc.code in _UNAVAILABLE_CODES: pytest.skip(f"LLM 服务/账号当前不可用(HTTP {exc.code}),跳过真实集成测试") raise def probe_llm_or_skip(model: str | None = None) -> None: """向真实 LLM 接口发一次最小请求,服务/账号不可用时跳过测试。 用途:被测函数(如 `nodes/subtitle_correction.correct_entry`)出于生产 需要会把调用异常吞掉并返回空串,测试因此无法从返回值区分"服务不可用" 与"模型没有泛化"。此探针在断言之前把外部状态问题显式暴露出来并跳过, 使断言只针对真实的能力回归。 探测失败判定与 `skip_on_service_unavailable` 一致(401/402/403/429 跳过); 其余错误(5xx、网络、返回结构异常)向上抛出,仍视为需要修复的问题。 """ import json as _json import urllib.request require_llm_credentials() api_base = os.getenv("LLM_API_BASE", "https://api.siliconflow.cn/v1/chat/completions") api_key = os.getenv("LLM_API_KEY", "") body = { "model": model or os.getenv("LLM_MODEL", "Qwen/Qwen3.5-35B-A3B"), "messages": [{"role": "user", "content": "ping"}], "max_tokens": 1, "enable_thinking": False, } request = urllib.request.Request( api_base, data=_json.dumps(body).encode("utf-8"), headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}, method="POST", ) with skip_on_service_unavailable(): try: with urllib.request.urlopen(request, timeout=30) as response: _json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError: raise