Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend + LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini -> DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials, RBAC, audit log, project-memory + Obsidian integrations, and editable troubleshooting decision rules tuned for the GeneseasX vessel stack. Co-authored-by: Cursor <cursoragent@cursor.com>
86 lines
2.9 KiB
Python
86 lines
2.9 KiB
Python
"""LLM routing configuration API."""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
from app.core.deps import require_admin, require_readonly
|
|
from app.models.user import User
|
|
from app.schemas import LlmRoutingConfigIn, LlmRoutingConfigOut, ProviderModelsOut
|
|
from app.services.model_catalog import catalog_by_tier, get_entry
|
|
from app.services.model_config import (
|
|
LlmRoutingConfig,
|
|
available_backends,
|
|
get_routing_config,
|
|
save_routing_config,
|
|
)
|
|
from app.services.model_discovery import list_all_provider_models
|
|
|
|
router = APIRouter(prefix="/api/llm", tags=["llm"])
|
|
|
|
ROUTING_STRATEGY = (
|
|
"Local Ollama first for triage and reasoning. On escalation, cloud providers are tried "
|
|
"in order: Gemini API → DeepSeek → OpenRouter. Within each cost tier (economy, premium), "
|
|
"each provider is attempted before moving to the next tier."
|
|
)
|
|
|
|
_MODEL_ID_FIELDS = (
|
|
"local_model_id",
|
|
"economy_gemini_id",
|
|
"economy_deepseek_id",
|
|
"economy_openrouter_id",
|
|
"premium_gemini_id",
|
|
"premium_deepseek_id",
|
|
"premium_openrouter_id",
|
|
)
|
|
|
|
|
|
@router.get("/routing", response_model=LlmRoutingConfigOut)
|
|
async def get_llm_routing(_: User = Depends(require_readonly)):
|
|
cfg = await get_routing_config()
|
|
return LlmRoutingConfigOut(
|
|
config=cfg.to_dict(),
|
|
catalog=catalog_by_tier(),
|
|
available_backends=available_backends(),
|
|
strategy=ROUTING_STRATEGY,
|
|
)
|
|
|
|
|
|
@router.get("/models/available", response_model=ProviderModelsOut)
|
|
async def get_available_models(_: User = Depends(require_readonly)):
|
|
"""Live model lists from Ollama, Gemini, DeepSeek, and OpenRouter APIs."""
|
|
data = await list_all_provider_models()
|
|
return ProviderModelsOut(providers=data["providers"], routing_order=data["routing_order"])
|
|
|
|
|
|
@router.put("/routing", response_model=LlmRoutingConfigOut)
|
|
async def update_llm_routing(body: LlmRoutingConfigIn, _: User = Depends(require_admin)):
|
|
current = await get_routing_config()
|
|
data = body.model_dump(exclude_unset=True)
|
|
|
|
for field in (
|
|
*_MODEL_ID_FIELDS,
|
|
"auto_escalate",
|
|
"max_tier",
|
|
):
|
|
if field in data and data[field] is not None:
|
|
setattr(current, field, data[field])
|
|
|
|
if body.cloud_provider_order is not None:
|
|
current.cloud_provider_order = body.cloud_provider_order
|
|
|
|
for fid in _MODEL_ID_FIELDS:
|
|
val = getattr(current, fid)
|
|
if val and not get_entry(val):
|
|
raise HTTPException(status_code=400, detail=f"Unknown model id: {val}")
|
|
|
|
if current.max_tier not in ("local", "economy", "premium"):
|
|
raise HTTPException(status_code=400, detail="max_tier must be local, economy, or premium")
|
|
|
|
await save_routing_config(current)
|
|
return LlmRoutingConfigOut(
|
|
config=current.to_dict(),
|
|
catalog=catalog_by_tier(),
|
|
available_backends=available_backends(),
|
|
strategy=ROUTING_STRATEGY,
|
|
)
|