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
+2 -2
View File
@@ -6,7 +6,6 @@ from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from backend.ai import (
DEFAULT_PROVIDER,
PROVIDERS,
ai_change_tone,
ai_continue_writing,
@@ -24,6 +23,7 @@ from backend.ai import (
ai_simplify,
ai_summarize,
ai_translate,
get_default_provider,
)
from backend.schemas import AIStatusResponse
@@ -91,7 +91,7 @@ async def api_status():
return {
"configured": any(p["available"] for p in providers.values()),
"default_provider": DEFAULT_PROVIDER,
"default_provider": get_default_provider(),
"providers": providers,
"autocomplete": autocomplete,
}
+8
View File
@@ -4286,6 +4286,8 @@ _DEFAULT_CONFIG = {
"prefix_max_expansions": 50,
"recent_files_limit": 20,
"max_backups_per_file": 10,
"ai_default_provider": "deepseek",
"ai_default_models": {},
}
@@ -4341,6 +4343,12 @@ async def api_set_config(body: dict = Body(...), current_user=Depends(require_ad
detail=f"Invalid type for '{key}': expected {expected_type.__name__}, got {type(value).__name__}",
)
_save_config(current)
if any(k.startswith("ai_") for k in updated_keys):
try:
from backend.ai import reload_ai_config
reload_ai_config()
except Exception as e:
logger.warning(f"Failed to reload AI config: {e}")
logger.info(f"Config updated: {updated_keys}")
return current