"""Endpoints for inspecting and testing the external LLM hookup. The LLM is not hosted here — the user supplies a base URL + model + key. """ from __future__ import annotations from fastapi import APIRouter, HTTPException from pydantic import BaseModel from api.lib.llm import ( LLMConfig, LLMError, chat_complete, from_settings, list_models, ping, ) router = APIRouter(prefix="/llm", tags=["llm"]) class LLMConfigIn(BaseModel): base_url: str api_key: str = "" # model is optional here so /llm/models can be called before a model is picked model: str = "" timeout: int = 120 max_tokens: int = 1000 temperature: float = 0.3 extra_headers: dict[str, str] = {} class LLMStatus(BaseModel): configured: bool base_url: str | None = None model: str | None = None class ChatIn(BaseModel): config: LLMConfigIn | None = None messages: list[dict] @router.get("/config", response_model=LLMStatus) async def get_status(): cfg = from_settings() if cfg is None: return LLMStatus(configured=False) return LLMStatus(configured=True, base_url=cfg.base_url, model=cfg.model) @router.post("/test") async def test_connection(cfg: LLMConfigIn | None = None): """Send a tiny ping to verify the endpoint accepts our requests.""" config = LLMConfig(**cfg.model_dump()) if cfg else from_settings() if config is None: raise HTTPException( status_code=400, detail="No LLM config in body and none configured in environment", ) try: reply = ping(config) except LLMError as e: raise HTTPException(status_code=502, detail=str(e)) return {"ok": True, "model": config.model, "reply": reply} @router.post("/models") async def models(cfg: LLMConfigIn | None = None): """List available models from the configured (or supplied) endpoint.""" config = LLMConfig(**cfg.model_dump()) if cfg else from_settings() if config is None: raise HTTPException( status_code=400, detail="No LLM config in body and none configured in environment", ) try: items = list_models(config) except LLMError as e: raise HTTPException(status_code=502, detail=str(e)) return {"models": items, "count": len(items)} @router.post("/chat") async def chat(body: ChatIn): """Pass-through chat endpoint — useful for sanity-testing prompts.""" config = LLMConfig(**body.config.model_dump()) if body.config else from_settings() if config is None: raise HTTPException(status_code=400, detail="No LLM config supplied") try: reply = chat_complete(config, body.messages) except LLMError as e: raise HTTPException(status_code=502, detail=str(e)) return {"reply": reply, "model": config.model}