"""Admin router for system settings — LLM and image provider configuration.""" import asyncio import os import time from typing import Optional, List from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from models.sql.user import UserModel from services.database import get_async_session from utils.auth_dependencies import require_client_admin SETTINGS_ROUTER = APIRouter(tags=["Admin - Settings"]) class SystemSettings(BaseModel): llm_provider: Optional[str] = None llm_model: Optional[str] = None image_provider: Optional[str] = None anthropic_api_key_set: bool = False openai_api_key_set: bool = False google_api_key_set: bool = False class SettingsUpdate(BaseModel): llm_provider: Optional[str] = None llm_model: Optional[str] = None image_provider: Optional[str] = None anthropic_api_key: Optional[str] = None openai_api_key: Optional[str] = None google_api_key: Optional[str] = None LLM_PROVIDERS = ["anthropic", "openai", "google", "ollama", "custom"] IMAGE_PROVIDERS = ["nanobanana_pro", "gemini_flash", "dall-e-3", "gpt-image-1.5", "pexels", "pixabay", "comfyui"] @SETTINGS_ROUTER.get("/settings") async def get_settings( admin: UserModel = Depends(require_client_admin), _session: AsyncSession = Depends(get_async_session), ): """Return current system settings (env-based config).""" if admin.role != "super_admin": raise HTTPException(status_code=403, detail="Super admin only") return { "llm_provider": os.getenv("LLM", "anthropic"), "llm_model": _get_current_model(), "image_provider": os.getenv("IMAGE_PROVIDER", "google"), "anthropic_api_key_set": bool(os.getenv("ANTHROPIC_API_KEY")), "openai_api_key_set": bool(os.getenv("OPENAI_API_KEY")), "google_api_key_set": bool(os.getenv("GOOGLE_API_KEY")), "available_llm_providers": LLM_PROVIDERS, "available_image_providers": IMAGE_PROVIDERS, } @SETTINGS_ROUTER.put("/settings") async def update_settings( body: SettingsUpdate, admin: UserModel = Depends(require_client_admin), session: AsyncSession = Depends(get_async_session), ): """Update system settings. Changes apply immediately and persist to database.""" if admin.role != "super_admin": raise HTTPException(status_code=403, detail="Super admin only") # Validate providers if body.llm_provider is not None and body.llm_provider not in LLM_PROVIDERS: raise HTTPException(status_code=400, detail=f"Invalid LLM provider: {body.llm_provider}") if body.image_provider is not None and body.image_provider not in IMAGE_PROVIDERS: raise HTTPException(status_code=400, detail=f"Invalid image provider: {body.image_provider}") from services.settings_service import save_settings data = body.model_dump(exclude_none=True) changed = await save_settings(session, data) return {"ok": True, "changed": changed} @SETTINGS_ROUTER.get("/settings/models") async def list_models( provider: str = Query(..., description="Provider name"), admin: UserModel = Depends(require_client_admin), _session: AsyncSession = Depends(get_async_session), ): """List available models for a given provider.""" if admin.role != "super_admin": raise HTTPException(status_code=403, detail="Super admin only") try: models = await _fetch_models_for_provider(provider) return {"provider": provider, "models": models} except Exception as e: return {"provider": provider, "models": [], "error": str(e)} class ConnectionTestRequest(BaseModel): provider: str api_key: Optional[str] = None @SETTINGS_ROUTER.post("/settings/test-connection") async def test_connection( body: ConnectionTestRequest, admin: UserModel = Depends(require_client_admin), _session: AsyncSession = Depends(get_async_session), ): """Test connectivity to an LLM/API provider.""" if admin.role != "super_admin": raise HTTPException(status_code=403, detail="Super admin only") start = time.time() try: ok, error = await _test_provider_connection(body.provider, body.api_key) latency_ms = int((time.time() - start) * 1000) return {"ok": ok, "error": error, "latency_ms": latency_ms} except Exception as e: latency_ms = int((time.time() - start) * 1000) return {"ok": False, "error": str(e), "latency_ms": latency_ms} def _get_current_model() -> str: provider = os.getenv("LLM", "anthropic") model_env_map = { "anthropic": "ANTHROPIC_MODEL", "openai": "OPENAI_MODEL", "google": "GOOGLE_MODEL", "ollama": "OLLAMA_MODEL", "custom": "CUSTOM_MODEL", } from constants.llm import DEFAULT_ANTHROPIC_MODEL, DEFAULT_OPENAI_MODEL, DEFAULT_GOOGLE_MODEL defaults = { "anthropic": DEFAULT_ANTHROPIC_MODEL, "openai": DEFAULT_OPENAI_MODEL, "google": DEFAULT_GOOGLE_MODEL, } env_key = model_env_map.get(provider, "ANTHROPIC_MODEL") return os.getenv(env_key, defaults.get(provider, "")) # --- Known model lists (for providers without a list API) --- ANTHROPIC_MODELS = [ "claude-opus-4-6", "claude-sonnet-4-6", "claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001", "claude-sonnet-4-5-20250514", "claude-3-5-haiku-20241022", ] GOOGLE_MODELS = [ "models/gemini-2.5-flash", "models/gemini-2.5-pro", "models/gemini-2.0-flash", "models/gemini-2.0-flash-lite", "models/gemini-1.5-flash", "models/gemini-1.5-pro", ] async def _fetch_models_for_provider(provider: str) -> List[str]: """Return a list of model IDs for the given provider.""" if provider == "anthropic": return ANTHROPIC_MODELS if provider == "google": # Try listing via API, fallback to hardcoded key = os.getenv("GOOGLE_API_KEY") if key: try: def _list_google(): import google.genai as genai client = genai.Client(api_key=key) result = [] for m in client.models.list(): name = getattr(m, "name", str(m)) if "gemini" in name.lower(): result.append(name) return sorted(result) return await asyncio.to_thread(_list_google) except Exception: pass return GOOGLE_MODELS if provider == "openai": key = os.getenv("OPENAI_API_KEY") if key: try: def _list_openai(): from openai import OpenAI client = OpenAI(api_key=key) models = client.models.list() return sorted([m.id for m in models.data if "gpt" in m.id or "o1" in m.id or "o3" in m.id or "o4" in m.id]) return await asyncio.to_thread(_list_openai) except Exception: pass return ["gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "gpt-4o", "gpt-4o-mini", "o3", "o4-mini"] if provider == "ollama": try: import httpx async with httpx.AsyncClient(timeout=5) as client: resp = await client.get(os.getenv("OLLAMA_URL", "http://localhost:11434") + "/api/tags") if resp.status_code == 200: data = resp.json() return [m["name"] for m in data.get("models", [])] except Exception: pass return [] return [] async def _test_provider_connection(provider: str, api_key: Optional[str] = None) -> tuple: """Test provider connection. Returns (ok: bool, error: str | None).""" if provider == "anthropic": key = api_key or os.getenv("ANTHROPIC_API_KEY") if not key: return False, "No API key configured" try: def _test(): import anthropic client = anthropic.Anthropic(api_key=key) client.messages.create( model="claude-haiku-4-5-20251001", max_tokens=5, messages=[{"role": "user", "content": "hi"}], ) return True await asyncio.to_thread(_test) return True, None except Exception as e: return False, str(e) if provider == "openai": key = api_key or os.getenv("OPENAI_API_KEY") if not key: return False, "No API key configured" try: def _test(): from openai import OpenAI client = OpenAI(api_key=key) client.models.list() return True await asyncio.to_thread(_test) return True, None except Exception as e: return False, str(e) if provider == "google": key = api_key or os.getenv("GOOGLE_API_KEY") if not key: return False, "No API key configured" try: def _test(): import google.genai as genai client = genai.Client(api_key=key) list(client.models.list())[:1] return True await asyncio.to_thread(_test) return True, None except Exception as e: return False, str(e) if provider == "ollama": try: import httpx async with httpx.AsyncClient(timeout=5) as client: resp = await client.get(os.getenv("OLLAMA_URL", "http://localhost:11434") + "/api/tags") if resp.status_code == 200: return True, None return False, f"HTTP {resp.status_code}" except Exception as e: return False, str(e) return False, f"Unknown provider: {provider}"