PMFlow/scripts/model_probe.py

285 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""独立模型连通性检测脚本(中文日志输出)。"""
from __future__ import annotations
import argparse
import json
import os
import ssl
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
BACKEND_DIR = REPO_ROOT / "backend"
if str(BACKEND_DIR) not in sys.path:
sys.path.insert(0, str(BACKEND_DIR))
from app.services.model.model_config import get_provider_defaults, get_tier_defaults
def load_env_file(env_path: Path) -> None:
if not env_path.exists():
return
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
if len(value) >= 2 and ((value[0] == value[-1] == '"') or (value[0] == value[-1] == "'")):
value = value[1:-1]
os.environ.setdefault(key, value)
def provider_runtime(provider: str, provider_defaults: dict[str, dict[str, str]]) -> dict[str, str]:
aliases = {"zhipu": "glm", "chatglm": "glm", "deep-seek": "deepseek", "local": "ollama"}
p = aliases.get(provider.strip().lower(), provider.strip().lower())
defaults = dict(provider_defaults.get(p) or {})
api_env = str(defaults.get("api_env") or "").strip()
api_key = str(os.getenv(api_env, "")).strip() if api_env else ""
if p == "glm":
api_key = str(os.getenv("DASHSCOPE_API_KEY", api_key)).strip()
base_url = str(defaults.get("base_url") or "").strip()
if p == "ollama" and not base_url:
base_url = str(os.getenv("OLLAMA_BASE_URL", os.getenv("OLLAMA_HOST", "http://127.0.0.1:11434"))).strip()
return {"provider": p, "provider_name": str(defaults.get("name") or p).strip(), "base_url": base_url, "api_key": api_key}
def extract_json_detail(text: str) -> dict[str, str]:
detail = {
"content": "",
"reasoning_content": "",
"error_message": "",
"body_preview": text[:180].replace("\n", " "),
}
try:
data = json.loads(text)
except Exception:
return detail
if isinstance(data, dict):
choices = data.get("choices") or []
if isinstance(choices, list) and choices:
first = choices[0] if isinstance(choices[0], dict) else {}
msg = first.get("message") if isinstance(first, dict) else {}
if isinstance(msg, dict):
detail["content"] = str(msg.get("content") or "").strip()
detail["reasoning_content"] = str(msg.get("reasoning_content") or "").strip()
if not detail["error_message"]:
err = data.get("error")
if isinstance(err, dict):
detail["error_message"] = str(err.get("message") or "").strip()
elif err:
detail["error_message"] = str(err).strip()
if not detail["error_message"]:
if data.get("message"):
detail["error_message"] = str(data.get("message")).strip()
elif data.get("reason"):
detail["error_message"] = str(data.get("reason")).strip()
return detail
def probe_model(
level: str,
provider: str,
model: str,
timeout: float,
insecure: bool,
provider_defaults: dict[str, dict[str, str]],
) -> dict[str, object]:
runtime = provider_runtime(provider, provider_defaults)
base_url = runtime["base_url"].rstrip("/")
api_key = runtime["api_key"]
started = time.perf_counter()
if not base_url:
elapsed = time.perf_counter() - started
return {
"level": level,
"provider": provider,
"model": model,
"ok": False,
"elapsed": elapsed,
"reason": f"provider={provider} 缺少 base_url 配置",
"http_status": 0,
}
url = f"{base_url}/chat/completions"
payload = {
"model": model,
"messages": [
{"role": "system", "content": "你是连通性检测助手,只回复OK。"},
{"role": "user", "content": "ping"},
],
"temperature": 0,
"max_tokens": 16,
}
req = urllib.request.Request(
url=url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
if api_key:
req.add_header("Authorization", f"Bearer {api_key}")
status_code = 0
body = ""
error_text = ""
ssl_context = ssl._create_unverified_context() if insecure else None
try:
with urllib.request.urlopen(req, timeout=timeout, context=ssl_context) as resp:
status_code = int(resp.getcode() or 0)
body = resp.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as exc:
status_code = int(exc.code or 0)
body = exc.read().decode("utf-8", errors="replace")
error_text = str(exc)
except Exception as exc: # pragma: no cover
error_text = str(exc)
elapsed = time.perf_counter() - started
detail = extract_json_detail(body)
content = detail["content"]
reasoning = detail["reasoning_content"]
has_text = bool(content)
has_reasoning = bool(reasoning)
http_ok = 200 <= status_code < 300
if http_ok and (has_text or has_reasoning):
note = "文本返回" if has_text else "仅推理内容"
return {
"level": level,
"provider": provider,
"model": model,
"ok": True,
"elapsed": elapsed,
"reason": note,
"http_status": status_code,
}
reason_parts: list[str] = []
if status_code:
reason_parts.append(f"HTTP {status_code}")
if detail["error_message"]:
reason_parts.append(str(detail["error_message"]))
elif error_text:
reason_parts.append(error_text)
elif http_ok:
reason_parts.append("响应为空(content/reasoning_content 均为空)")
else:
reason_parts.append(detail["body_preview"] or "请求失败")
return {
"level": level,
"provider": provider,
"model": model,
"ok": False,
"elapsed": elapsed,
"reason": " | ".join(reason_parts),
"http_status": status_code,
}
def render_result_line(item: dict[str, object]) -> str:
status_text = "✓ 正常" if bool(item["ok"]) else "✗ 失败"
elapsed_text = f"{float(item['elapsed']):.2f}s"
return (
f"[{item['level']}] {item['model']} ({item.get('provider_name') or item['provider']}) "
f"{status_text} ({elapsed_text})"
)
def main() -> int:
parser = argparse.ArgumentParser(description="PMFlow 模型连通性独立检测")
parser.add_argument(
"--env-file",
default=str(Path(__file__).resolve().parents[1] / ".env"),
help="环境变量文件路径(默认根目录 .env)",
)
parser.add_argument(
"--timeout",
type=float,
default=float(os.getenv("PMFLOW_MODEL_PROBE_TIMEOUT", "20")),
help="单次请求超时时间(秒)",
)
parser.add_argument(
"--insecure",
action="store_true",
default=str(os.getenv("PMFLOW_MODEL_PROBE_INSECURE", "1")).strip().lower() in {"1", "true", "yes"},
help="关闭 TLS 证书校验(默认开启,可用 PMFLOW_MODEL_PROBE_INSECURE=0 关闭)",
)
parser.add_argument(
"--strict-tls",
action="store_true",
default=False,
help="强制开启 TLS 证书校验(优先级高于 --insecure)",
)
args = parser.parse_args()
load_env_file(Path(args.env_file))
insecure = bool(args.insecure and not args.strict_tls)
provider_defaults = get_provider_defaults(refresh=True)
tier_defaults = get_tier_defaults(refresh=True)
low_tier = dict(tier_defaults.get("low") or {})
high_tier = dict(tier_defaults.get("high") or {})
low_provider = str(low_tier.get("provider") or "glm").strip() or "glm"
low_model = str(low_tier.get("model") or provider_defaults.get(low_provider, {}).get("model") or "qwen-plus").strip() or "qwen-plus"
high_provider = str(high_tier.get("provider") or "openai").strip() or "openai"
high_model = str(high_tier.get("model") or provider_defaults.get(high_provider, {}).get("model") or "GLM-5").strip() or "GLM-5"
targets: list[tuple[str, str, str]] = [("LOW", low_provider, low_model), ("HIGH", high_provider, high_model)]
deduped: list[tuple[str, str, str]] = []
seen: set[tuple[str, str]] = set()
for level, provider, model in targets:
key = (provider.lower(), model)
if key in seen:
continue
seen.add(key)
deduped.append((level, provider, model))
started_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print("┌────────────────────────────────────────┐")
print("│ PMFlow 模型连通性独立检测 │")
print(f"│ 启动时间: {started_at} │")
print(f"│ TLS校验: {'关闭' if insecure else '开启'} │")
print("├────────────────────────────────────────┤")
results: list[dict[str, object]] = []
for level, provider, model in deduped:
result = probe_model(
level=level,
provider=provider,
model=model,
timeout=args.timeout,
insecure=insecure,
provider_defaults=provider_defaults,
)
results.append(result)
print(f"│ {render_result_line(result)}")
print(f"│ 说明: {result['reason']}")
print("└────────────────────────────────────────┘")
failed = [item for item in results if not bool(item["ok"])]
if failed:
print(f"[结果] 检测完成:{len(failed)} 个模型异常。")
return 1
print("[结果] 检测完成:全部模型连通正常。")
return 0
if __name__ == "__main__":
sys.exit(main())