feat(server): 新增 LLM 状态/成本 API 与规则配置 API
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
"""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"]
|
||||
Reference in New Issue
Block a user