Live AI provider/model switching without restart
CI / Lint & Format (push) Failing after 8s
CI / Tests (push) Has been skipped
CI / Security Scan (push) Failing after 8s
CI / Docker Build (push) Has been skipped

Backend:
- RuntimeAIConfig singleton: mutable in-memory overrides for provider/model
- PATCH /admin/api/ai/config: change active provider/model on the fly
- GET /admin/api/ai/config: read current runtime state
- ai_vision.py: uses runtime_ai_config before falling back to settings
- health/status endpoints reflect runtime overrides

Frontend:
- 'Définir comme actif' button in AI config section
- Live feedback message on save
This commit is contained in:
2026-06-25 11:07:17 -04:00
parent 7dd26ea636
commit 92cf23dbb3
6 changed files with 216 additions and 14 deletions
+69 -3
View File
@@ -17,6 +17,7 @@ from app.config import settings
from app.database import get_db
from app.dependencies.auth import require_scope
from app.models.client import APIClient
from app.services.runtime_config import runtime_ai_config
logger = logging.getLogger(__name__)
@@ -114,13 +115,14 @@ def _build_providers() -> list[ProviderInfo]:
@router.get("/status", response_model=AIStatusResponse)
async def get_ai_status():
"""Retourne la configuration AI actuelle et les fournisseurs/modèles disponibles."""
active_model = (
settings.OPENROUTER_MODEL if settings.AI_PROVIDER == "openrouter"
active_provider = runtime_ai_config.provider or settings.AI_PROVIDER
active_model = runtime_ai_config.model or (
settings.OPENROUTER_MODEL if active_provider == "openrouter"
else settings.GEMINI_MODEL
)
return AIStatusResponse(
ai_enabled=settings.AI_ENABLED,
current_provider=settings.AI_PROVIDER,
current_provider=active_provider,
current_model=active_model,
providers=_build_providers(),
)
@@ -252,3 +254,67 @@ async def _test_gemini(model: str, prompt: Optional[str], image_b64: Optional[st
return {"success": True, "response": text[:500], "tokens": tokens}
except Exception as e:
return {"success": False, "error": str(e)[:500]}
# ─────────────────────────────────────────────────────────────
# GET /admin/api/ai/config — état actuel du runtime config
# ─────────────────────────────────────────────────────────────
@router.get("/config")
async def get_runtime_config():
"""Retourne la configuration AI runtime actuelle (provider + modèle)."""
return runtime_ai_config.to_dict()
# ─────────────────────────────────────────────────────────────
# PATCH /admin/api/ai/config — changer le provider/modèle live
# ─────────────────────────────────────────────────────────────
class UpdateAIConfigRequest(BaseModel):
provider: Optional[str] = Field(None, description="openrouter ou gemini")
model: Optional[str] = Field(None, description="Nom du modèle à activer")
reset: bool = Field(False, description="true = réinitialiser aux valeurs .env")
class UpdateAIConfigResponse(BaseModel):
provider: str
model: str
is_overridden: bool
message: str
@router.patch("/config", response_model=UpdateAIConfigResponse)
async def update_runtime_config(
body: UpdateAIConfigRequest,
_=Depends(require_scope("admin")),
):
"""Change le fournisseur et/ou le modèle AI actif sans redémarrer."""
if body.reset:
runtime_ai_config.clear()
state = runtime_ai_config.to_dict()
return UpdateAIConfigResponse(
provider=state["provider"],
model=state["model"],
is_overridden=False,
message="Configuration AI réinitialisée aux valeurs .env",
)
if not body.provider and not body.model:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Spécifiez provider et/ou model, ou reset=true",
)
runtime_ai_config.set(provider=body.provider, model=body.model)
state = runtime_ai_config.to_dict()
logger.info("ai.runtime_config.updated", extra={
"provider": state["provider"],
"model": state["model"],
})
return UpdateAIConfigResponse(
provider=state["provider"],
model=state["model"],
is_overridden=True,
message=f"Fournisseur changé à {state['provider']}, modèle {state['model']}",
)