- новый api/: /pipeline/process (yt-dlp, субтитры или Vosk, LLM/экстрактивная суммаризация), /videos, /llm; старый код перенесён в api_legacy/ - web/: Flet UI (flet 0.28.3 + flet-web, порт 8550) - Dockerfile.api: ffmpeg слоем из mwader/static-ffmpeg, pip с кэш-маунтом - requirements/pyproject: убраны moviepy, imageio-ffmpeg, битый asyncio - README с инструкцией запуска и загрузкой Vosk-модели Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
96 lines
2.8 KiB
Python
96 lines
2.8 KiB
Python
"""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}
|