IIIF-Studio / backend /app /api /v1 /models_api.py
Claude
fix: comprehensive repo audit — 15 issues fixed
9097545 unverified
"""
Endpoints de gestion des providers et modèles IA (R10 — préfixe /api/v1/).
GET /api/v1/providers → providers détectés (disponibles ou non)
GET /api/v1/providers/{provider_type}/models → modèles d'un provider
POST /api/v1/models/refresh → liste agrégée de tous les modèles
PUT /api/v1/corpora/{id}/model → associe un modèle à un corpus
GET /api/v1/corpora/{id}/model → modèle actif d'un corpus
Les clés API vivent exclusivement dans les secrets HuggingFace (variables
d'environnement). Le backend détecte automatiquement quels providers sont
disponibles au démarrage. L'interface ne demande jamais de clé (R06).
"""
# 1. stdlib
import logging
from datetime import datetime, timezone
# 2. third-party
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.ext.asyncio import AsyncSession
# 3. local
from app.models.corpus import CorpusModel
from app.models.database import get_db
from app.models.model_config_db import ModelConfigDB
from app.schemas.model_config import ProviderType
logger = logging.getLogger(__name__)
router = APIRouter(tags=["models"])
# ── Schémas ───────────────────────────────────────────────────────────────────
class ProviderInfo(BaseModel):
"""Informations sur un provider IA détecté au démarrage."""
provider_type: str
display_name: str
available: bool
model_count: int
class ModelSelectRequest(BaseModel):
model_id: str = Field(..., min_length=1, max_length=256)
provider_type: str = Field(..., min_length=1, max_length=64)
display_name: str = Field("", max_length=256)
supports_vision: bool = Field(True)
class ModelConfigResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
corpus_id: str
provider_type: str
selected_model_id: str
selected_model_display_name: str
supports_vision: bool
updated_at: datetime
class ModelsRefreshResponse(BaseModel):
models: list[dict]
count: int
refreshed_at: datetime
# ── Endpoints ─────────────────────────────────────────────────────────────────
@router.get("/providers", response_model=list[ProviderInfo])
async def list_providers() -> list[dict]:
"""Liste tous les providers IA avec leur disponibilité.
Un provider est disponible si la variable d'environnement correspondante
est présente dans les secrets HuggingFace. Aucune clé n'est exposée.
"""
from app.services.ai.model_registry import get_available_providers
return get_available_providers()
@router.get("/providers/{provider_type}/models", response_model=list[dict])
async def get_provider_models(provider_type: str) -> list[dict]:
"""Liste les modèles disponibles pour un provider spécifique."""
try:
ptype = ProviderType(provider_type)
except ValueError:
raise HTTPException(
status_code=404,
detail=f"Provider inconnu : {provider_type}. "
f"Valeurs acceptées : {[p.value for p in ProviderType]}",
)
from app.services.ai.model_registry import list_models_for_provider
try:
models = list_models_for_provider(ptype)
except RuntimeError as exc:
raise HTTPException(status_code=503, detail=str(exc))
except Exception as exc:
logger.warning("Erreur listing models", extra={"provider": provider_type, "error": str(exc)})
raise HTTPException(status_code=502, detail="Erreur temporaire du provider IA. Réessayez ultérieurement.")
return [m.model_dump() for m in models]
@router.post("/models/refresh", response_model=ModelsRefreshResponse)
async def refresh_models() -> ModelsRefreshResponse:
"""Force la mise à jour de la liste agrégée de tous les modèles disponibles."""
from app.services.ai.model_registry import list_all_models
models = list_all_models()
return ModelsRefreshResponse(
models=[m.model_dump() for m in models],
count=len(models),
refreshed_at=datetime.now(timezone.utc),
)
@router.put("/corpora/{corpus_id}/model", response_model=ModelConfigResponse)
async def set_corpus_model(
corpus_id: str,
body: ModelSelectRequest,
db: AsyncSession = Depends(get_db),
) -> ModelConfigResponse:
"""Associe un modèle IA à un corpus. Crée ou met à jour la configuration."""
corpus = await db.get(CorpusModel, corpus_id)
if corpus is None:
raise HTTPException(status_code=404, detail="Corpus introuvable")
display_name = body.display_name or body.model_id
config = await db.get(ModelConfigDB, corpus_id)
if config is None:
config = ModelConfigDB(
corpus_id=corpus_id,
provider_type=body.provider_type,
selected_model_id=body.model_id,
selected_model_display_name=display_name,
supports_vision=body.supports_vision,
updated_at=datetime.now(timezone.utc),
)
db.add(config)
else:
config.provider_type = body.provider_type
config.selected_model_id = body.model_id
config.selected_model_display_name = display_name
config.supports_vision = body.supports_vision
config.updated_at = datetime.now(timezone.utc)
await db.commit()
await db.refresh(config)
return config
@router.get("/corpora/{corpus_id}/model", response_model=ModelConfigResponse)
async def get_corpus_model(
corpus_id: str, db: AsyncSession = Depends(get_db)
) -> ModelConfigResponse:
"""Retourne la configuration du modèle IA actif pour un corpus."""
corpus = await db.get(CorpusModel, corpus_id)
if corpus is None:
raise HTTPException(status_code=404, detail="Corpus introuvable")
config = await db.get(ModelConfigDB, corpus_id)
if config is None:
raise HTTPException(
status_code=404,
detail="Aucun modèle configuré pour ce corpus",
)
return config