"""后台管理:LLM 专家/终裁配置、可用模型探测、连通性测试。 所有接口需管理员鉴权(require_admin)。路由前缀 /api/v1/admin。 """ from __future__ import annotations import logging import time import httpx from fastapi import APIRouter, Depends from src.api.deps import require_admin from src.core.config import settings from src.core.http_client import get_client from src.core.runtime_config import ( AGENT_META, SETTING_DEFS, get_runtime_value, get_setting_origin, mask_value, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/admin", tags=["admin"], dependencies=[Depends(require_admin)]) @router.get("/llm/agents") async def list_llm_agents(): """各专家/终裁的独立 LLM 配置状态(含当前生效模型的解析结果)。""" out = [] for agent in AGENT_META: aid = agent["id"].upper() pfx = f"AGENT_{aid}_" fields = {} for suffix in ("MODEL", "BASE_URL", "API_KEY"): origin, value = await get_setting_origin(f"{pfx}{suffix}") defn = SETTING_DEFS[f"{pfx}{suffix}"] fields[suffix.lower()] = { "configured": origin != "none", "masked": mask_value(value, defn.sensitive), "origin": origin, } # 生效模型 = 覆盖 → 层级默认(专家/终裁 env) → 全局 LLM_MODEL tier_default = ( settings.LLM_AGGREGATOR_MODEL if agent["id"] == "aggregator" else settings.LLM_SPECIALIST_MODEL ) effective_model = ( fields["model"]["masked"] if fields["model"]["configured"] else (tier_default or await get_runtime_value("LLM_MODEL")) ) out.append( { "id": agent["id"], "label": agent["label"], "fields": fields, "effective_model": effective_model, } ) return out @router.get("/llm/models") async def list_llm_models(): """探测当前 LLM 服务可用的模型列表(OpenAI 兼容 GET /models)。 只读探测,不产生费用;配置缺失或服务不可达时返回 ok=false 与原因。 """ base_url = (await get_runtime_value("LLM_BASE_URL")).rstrip("/") api_key = await get_runtime_value("LLM_API_KEY") if not base_url or not api_key: return {"ok": False, "models": [], "detail": "LLM_BASE_URL 或 LLM_API_KEY 未配置"} client = get_client() start = time.monotonic() try: resp = await client.get( f"{base_url}/models", headers={"Authorization": f"Bearer {api_key}"}, timeout=httpx.Timeout(connect=10.0, read=20.0, write=10.0, pool=10.0), ) except Exception as e: return { "ok": False, "models": [], "latency_ms": int((time.monotonic() - start) * 1000), "detail": f"无法连接 LLM 服务: {e}", } latency = int((time.monotonic() - start) * 1000) if resp.status_code in (401, 403): return {"ok": False, "models": [], "latency_ms": latency, "detail": "密钥无效或无权限(HTTP 401/403)"} if resp.status_code != 200: return {"ok": False, "models": [], "latency_ms": latency, "detail": f"服务返回 HTTP {resp.status_code}"} try: data = resp.json() except Exception: return {"ok": False, "models": [], "latency_ms": latency, "detail": "响应不是合法 JSON"} models: list[str] = [] items = data.get("data") if isinstance(data, dict) else None if isinstance(items, list): models = sorted( str(m.get("id")) for m in items if isinstance(m, dict) and m.get("id") ) if not models: return {"ok": False, "models": [], "latency_ms": latency, "detail": "服务未返回模型列表"} return {"ok": True, "models": models, "latency_ms": latency, "detail": f"共 {len(models)} 个可用模型"} @router.post("/llm/ping") async def llm_ping(): """LLM 连通性测试(不依赖比赛)。只发一次 chat 请求验证配置。""" from src.llm.provider import get_default_provider p = await get_default_provider() resp = await p.chat( system="你是测试助手。", user="ping", max_tokens=10, ) if resp.error: return {"ok": False, "message": resp.error} return {"ok": True, "message": "LLM 连接正常", "model": p.model}