Live AI provider/model switching without restart
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:
@@ -22,6 +22,7 @@ from google.genai import types
|
||||
|
||||
from app.config import settings
|
||||
from app.services.storage_backend import get_storage_backend
|
||||
from app.services.runtime_config import runtime_ai_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -128,7 +129,7 @@ async def _generate_gemini(
|
||||
contents.append(types.Part.from_bytes(data=image_bytes, mime_type=media_type))
|
||||
contents.append(prompt)
|
||||
|
||||
model = model_override or settings.GEMINI_MODEL
|
||||
model = model_override or runtime_ai_config.model or settings.GEMINI_MODEL
|
||||
|
||||
async def _call():
|
||||
return await asyncio.wait_for(
|
||||
@@ -165,7 +166,7 @@ async def _generate_openrouter(
|
||||
logger.warning("ai.openrouter.no_key")
|
||||
return {"text": None, "usage": (None, None)}
|
||||
|
||||
model = model_override or settings.OPENROUTER_MODEL
|
||||
model = model_override or runtime_ai_config.model or settings.OPENROUTER_MODEL
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {settings.OPENROUTER_API_KEY}",
|
||||
@@ -233,7 +234,7 @@ async def _generate(
|
||||
model_override: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Dispatcher vers le bon provider, avec override optionnel par client."""
|
||||
provider = provider_override or settings.AI_PROVIDER.lower()
|
||||
provider = provider_override or runtime_ai_config.provider or settings.AI_PROVIDER.lower()
|
||||
logger.info("ai.generate", extra={"provider": provider})
|
||||
|
||||
if provider == "openrouter":
|
||||
@@ -289,8 +290,8 @@ async def analyze_image(
|
||||
if not settings.AI_ENABLED:
|
||||
return {}
|
||||
|
||||
active_provider = provider_override or settings.AI_PROVIDER
|
||||
active_model = model_override or (
|
||||
active_provider = provider_override or runtime_ai_config.provider or settings.AI_PROVIDER
|
||||
active_model = model_override or runtime_ai_config.model or (
|
||||
settings.OPENROUTER_MODEL if active_provider == "openrouter"
|
||||
else settings.GEMINI_MODEL
|
||||
)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Runtime AI Config — mutable overrides pour le provider/model actif.
|
||||
|
||||
Permet à l'admin de changer le fournisseur/modèle sans redémarrer.
|
||||
Les overrides sont stockés en mémoire (perdus au redémarrage du backend).
|
||||
"""
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class RuntimeAIConfig:
|
||||
"""Singleton thread-safe pour les overrides AI runtime."""
|
||||
|
||||
def __init__(self):
|
||||
self._lock = threading.Lock()
|
||||
self._provider: Optional[str] = None
|
||||
self._model: Optional[str] = None
|
||||
|
||||
@property
|
||||
def provider(self) -> Optional[str]:
|
||||
with self._lock:
|
||||
return self._provider
|
||||
|
||||
@property
|
||||
def model(self) -> Optional[str]:
|
||||
with self._lock:
|
||||
return self._model
|
||||
|
||||
@property
|
||||
def has_override(self) -> bool:
|
||||
with self._lock:
|
||||
return self._provider is not None
|
||||
|
||||
def set(self, provider: Optional[str] = None, model: Optional[str] = None) -> None:
|
||||
"""Définit les overrides. None = utiliser le défaut de settings."""
|
||||
with self._lock:
|
||||
self._provider = provider
|
||||
self._model = model
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Réinitialise aux valeurs par défaut."""
|
||||
with self._lock:
|
||||
self._provider = None
|
||||
self._model = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Retourne l'état actuel pour les API."""
|
||||
from app.config import settings
|
||||
with self._lock:
|
||||
return {
|
||||
"provider": self._provider or settings.AI_PROVIDER,
|
||||
"model": self._model or (
|
||||
settings.OPENROUTER_MODEL if (self._provider or settings.AI_PROVIDER) == "openrouter"
|
||||
else settings.GEMINI_MODEL
|
||||
),
|
||||
"is_overridden": self._provider is not None,
|
||||
"override_provider": self._provider,
|
||||
"override_model": self._model,
|
||||
}
|
||||
|
||||
|
||||
# Singleton global
|
||||
runtime_ai_config = RuntimeAIConfig()
|
||||
Reference in New Issue
Block a user