"""LLM 状态/成本/控制 API (MVP-3 / D34) 提供给前端的 RESTful 接口: - ``GET /llm/status`` 返回 LLM 是否启用、provider、熔断状态、降级原因 - ``GET /llm/cost`` 返回今日费用 / 预算 / token 用量 - ``GET /llm/cost/history`` 返回最近 N 天的日级统计 - ``GET /llm/cost/records`` 返回最近 N 次调用明细 - ``POST /llm/disable`` 手动禁用 LLM 调用 (运维) - ``POST /llm/enable`` 手动重新启用 - ``POST /llm/reset`` 重置成本统计 + 熔断状态 设计要点: - ``llm_service`` 与 ``cost_tracker`` 都通过 ``main.py`` 在 lifespan 中注入, 此处仅暴露读取接口 - 前端的"规则配置 / LLM 状态面板"通过本接口拿到实时数据 """ from __future__ import annotations import logging from typing import Any, Dict, List, Optional from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel, Field from core.settings import get_settings from services.llm_analysis_service import LLMAnalysisService from services.llm_cost_tracker import LLMCostTracker logger = logging.getLogger(__name__) router = APIRouter(prefix="/llm", tags=["LLM 二次判断"]) # --------------------------------------------------------------------------- # 全局注入 (由 main.py 调用) # --------------------------------------------------------------------------- _llm_service: Optional[LLMAnalysisService] = None _cost_tracker: Optional[LLMCostTracker] = None def init_llm_api( llm_service: Optional[LLMAnalysisService], cost_tracker: Optional[LLMCostTracker], ) -> None: """由 main.py 在启动阶段注入服务实例。""" global _llm_service, _cost_tracker _llm_service = llm_service _cost_tracker = cost_tracker logger.info( "LLM API 已初始化: service=%s tracker=%s", bool(llm_service), bool(cost_tracker), ) def _require_tracker() -> LLMCostTracker: if _cost_tracker is None: raise HTTPException( status_code=503, detail="LLMCostTracker 未初始化" ) return _cost_tracker # --------------------------------------------------------------------------- # 响应模型 # --------------------------------------------------------------------------- class LLMStatusResponse(BaseModel): enabled: bool provider: str model: str api_base: Optional[str] = None triggers: Dict[str, Any] = Field(default_factory=dict) fusion: Dict[str, Any] = Field(default_factory=dict) cost: Dict[str, Any] = Field(default_factory=dict) # --------------------------------------------------------------------------- # API 路由 # --------------------------------------------------------------------------- @router.get("/status", response_model=LLMStatusResponse) async def get_llm_status() -> LLMStatusResponse: """获取 LLM 当前运行状态 (供前端 LLM 状态面板)。""" settings = get_settings() llm_cfg = settings.llm trigger_cfg = settings.llm_trigger fusion_cfg = settings.fusion cost_data: Dict[str, Any] if _cost_tracker is not None: cost_data = _cost_tracker.stats() else: cost_data = {"enabled": llm_cfg.enabled, "circuit_state": "n/a"} provider_name = llm_cfg.provider if _llm_service is not None: provider_name = getattr(_llm_service.provider, "name", provider_name) return LLMStatusResponse( enabled=llm_cfg.enabled, provider=provider_name, model=llm_cfg.model, api_base=llm_cfg.api_base, triggers={ "enabled": trigger_cfg.enabled, "window_seconds": trigger_cfg.window_seconds, "min_consecutive_hits": trigger_cfg.min_consecutive_hits, "min_avg_confidence": trigger_cfg.min_avg_confidence, "cooldown_seconds": trigger_cfg.cooldown_seconds, "severity_bypass": trigger_cfg.severity_bypass, }, fusion={ "strategy": fusion_cfg.strategy, "yolo_weight": fusion_cfg.yolo_weight, "llm_weight": fusion_cfg.llm_weight, "suppress_on_llm_negative": fusion_cfg.suppress_on_llm_negative, "fallback_to_yolo": fusion_cfg.fallback_to_yolo, }, cost=cost_data, ) @router.get("/cost") async def get_llm_cost() -> Dict[str, Any]: """获取今日 LLM 成本概览。""" return _require_tracker().stats() @router.get("/cost/history") async def get_llm_cost_history( days: int = Query(default=7, ge=1, le=30), ) -> Dict[str, List[Dict[str, Any]]]: """最近 N 天的日级成本/调用统计。""" summary = _require_tracker().daily_summary() return {"days": summary[-days:]} @router.get("/cost/records") async def get_llm_cost_records( limit: int = Query(default=20, ge=1, le=200), ) -> Dict[str, List[Dict[str, Any]]]: """最近 N 次 LLM 调用明细。""" return {"records": _require_tracker().recent_records(limit=limit)} @router.post("/disable") async def disable_llm() -> Dict[str, Any]: """手动禁用 LLM 调用。""" tracker = _require_tracker() tracker.disable() return {"success": True, "enabled": tracker.is_enabled} @router.post("/enable") async def enable_llm() -> Dict[str, Any]: """手动重新启用 LLM 调用 (重置熔断)。""" tracker = _require_tracker() tracker.enable() return {"success": True, "enabled": tracker.is_enabled} @router.post("/reset") async def reset_llm() -> Dict[str, Any]: """重置成本统计与熔断状态。""" tracker = _require_tracker() tracker.reset() return {"success": True, "stats": tracker.stats()} __all__ = ["router", "init_llm_api"]