ppt-tool/backend/api/v1/admin/settings_router.py
Vadym Samoilenko d3d1667a79 Phase 2: Admin panel, analytics, storage, template pipeline, multi-provider LLM
- Fix admin sidebar: remove duplicate Teams, add Storage nav item
- Analytics: client-scoped queries, super_admin sees all (including NULL client_id)
- Storage management: list/download/delete presentations with file metadata
- Settings page with brand config router
- AI usage tracking: new AIUsageModel, ai_usage_service, analytics endpoint
- Master deck → template bridge: _register_as_template creates TemplateModel
  + PresentationLayoutCodeModel so parsed layouts appear in template picker
- Multi-provider LLM vision in parser: Anthropic/Google/OpenAI with asyncio.to_thread
- Fix PPTX upload 400: accept by .pptx extension (browser sends octet-stream)
- Fix reparse FK violation: presentation_id=None for parse_master_deck jobs
- Worker job_timeout increased to 1800s for LLM-heavy master deck parsing
- PYTHONUNBUFFERED=1 in docker-compose worker for real-time log output
- Auth: clientId in /me response, dev-login cookie improvements
- Frontend: auth slice clientId, master-deck thumbnails, storage page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 23:39:34 +00:00

128 lines
4.5 KiB
Python

"""Admin router for system settings — LLM and image provider configuration."""
import os
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
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 = ["google", "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 (runtime env vars). Changes don't persist across restarts."""
if admin.role != "super_admin":
raise HTTPException(status_code=403, detail="Super admin only")
changed = []
if body.llm_provider is not None:
if body.llm_provider not in LLM_PROVIDERS:
raise HTTPException(status_code=400, detail=f"Invalid LLM provider: {body.llm_provider}")
os.environ["LLM"] = body.llm_provider
changed.append("llm_provider")
if body.llm_model is not None:
provider = body.llm_provider or os.getenv("LLM", "anthropic")
model_env_map = {
"anthropic": "ANTHROPIC_MODEL",
"openai": "OPENAI_MODEL",
"google": "GOOGLE_MODEL",
"ollama": "OLLAMA_MODEL",
"custom": "CUSTOM_MODEL",
}
env_key = model_env_map.get(provider)
if env_key:
os.environ[env_key] = body.llm_model
changed.append("llm_model")
if body.image_provider is not None:
if body.image_provider not in IMAGE_PROVIDERS:
raise HTTPException(status_code=400, detail=f"Invalid image provider: {body.image_provider}")
os.environ["IMAGE_PROVIDER"] = body.image_provider
changed.append("image_provider")
if body.anthropic_api_key is not None:
os.environ["ANTHROPIC_API_KEY"] = body.anthropic_api_key
changed.append("anthropic_api_key")
if body.openai_api_key is not None:
os.environ["OPENAI_API_KEY"] = body.openai_api_key
changed.append("openai_api_key")
if body.google_api_key is not None:
os.environ["GOOGLE_API_KEY"] = body.google_api_key
changed.append("google_api_key")
return {"ok": True, "changed": changed}
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, ""))