feat(ai): selection fournisseur/modele par defaut + guide architecture

- Persistance ai_default_provider / ai_default_models dans data/config.json
- Lecture + rechargement a chaud dans backend/ai.py (get_default_provider, reload_ai_config)
- UI: selecteurs Fournisseur/Modele par defaut dans la section Cles API IA (i18n FR/EN)
- docs/AI_ARCHITECTURE_GUIDE.md: architecture cible, catalogue d'outils, decisions MCP
This commit is contained in:
2026-09-11 12:03:16 -04:00
parent e2153435dc
commit a3642caa3d
8 changed files with 451 additions and 8 deletions
+43 -3
View File
@@ -18,6 +18,7 @@ ProviderName = Literal["deepseek", "openrouter", "gemini", "ollama", "nvidia", "
# Provider configurations — keys loaded from file or .env
AI_KEYS_FILE = Path("data/api_keys.json")
APP_CONFIG_FILE = Path(__file__).resolve().parent.parent / "data" / "config.json"
def _read_ai_keys() -> dict:
if not AI_KEYS_FILE.exists():
@@ -27,6 +28,16 @@ def _read_ai_keys() -> dict:
except Exception:
return {}
def _read_app_config() -> dict:
"""Read the persisted application config (``data/config.json``)."""
if not APP_CONFIG_FILE.exists():
return {}
try:
return json.loads(APP_CONFIG_FILE.read_text(encoding="utf-8"))
except Exception:
return {}
def get_ai_key(env_name: str) -> str:
"""Get AI key: stored file first, then .env fallback."""
keys = _read_ai_keys()
@@ -36,7 +47,7 @@ def get_ai_key(env_name: str) -> str:
def _load_provider_keys():
"""Load AI keys from stored file, falling back to .env."""
return {
providers = {
"deepseek": {
"api_key": get_ai_key("DEEPSEEK_API_KEY"),
"base_url": "https://api.deepseek.com/v1",
@@ -90,17 +101,46 @@ def _load_provider_keys():
"auth_header": "Bearer {api_key}",
},
}
# Apply persisted per-provider model overrides (data/config.json).
overrides = _read_app_config().get("ai_default_models") or {}
if isinstance(overrides, dict):
for name, model in overrides.items():
if name in providers and isinstance(model, str) and model:
providers[name]["model"] = model
return providers
PROVIDERS = _load_provider_keys()
DEFAULT_PROVIDER: ProviderName = os.getenv("AI_DEFAULT_PROVIDER", "deepseek") # type: ignore
def get_default_provider() -> str:
"""Resolve the default provider: ``data/config.json`` > env > ``deepseek``."""
provider = _read_app_config().get("ai_default_provider") or os.getenv("AI_DEFAULT_PROVIDER", "deepseek")
return provider if provider in PROVIDERS else "deepseek"
def reload_ai_config() -> str:
"""Reload provider keys and model overrides from disk into ``PROVIDERS`` in place.
Mutating ``PROVIDERS`` (rather than rebinding it) keeps references held by
other modules valid. Returns the resolved default provider.
"""
global DEFAULT_PROVIDER
PROVIDERS.clear()
PROVIDERS.update(_load_provider_keys())
DEFAULT_PROVIDER = get_default_provider() # type: ignore[assignment]
logger.info(f"AI config reloaded (default provider: {DEFAULT_PROVIDER})")
return DEFAULT_PROVIDER
def _get_provider_config(provider: ProviderName | None = None) -> dict:
"""Get provider config, falling back to default if requested provider unavailable."""
p = provider or DEFAULT_PROVIDER
default = get_default_provider()
p = provider or default
if p not in PROVIDERS:
p = DEFAULT_PROVIDER
p = default
cfg = PROVIDERS[p]
if not cfg["api_key"]:
# Try next available provider