fix(agent): validation robuste des modeles Nvidia (payload production + repli 404/410) + logo noir/blanc
Validation: - _validate_chat_models utilise le payload de production (temperature 0.2, sans max_tokens) : un 200 a la validation == 200 a l'usage reel. - Race corrigee : payload reconstruit par modele (plus de noms croises entre requetes concurrentes) — la v5.1.8 retombait par intermittence sur la liste brute (68 modeles dont la plupart en 404/410). - Retente une fois sur timeout/5xx (les modeles lents mais fonctionnels survivent) ; rejette les 4xx (404 inconnu, 410 retire) ; garde les 429. - Verifie en live contre l'API NVIDIA : 12 modeles valides au lieu de 68. Repli runtime: - LLMClient._http_complete : sur 404/410, retente une fois avec le modele par defaut du provider et marque un notice dans LLMResponse. - AgentEngine emet un evenement SSE "notice" (bandeau .fd-ap-notice dans le panneau Agent, reset a chaque conversation) ; le modele reel est persistee. UI: - FAB (rond bas droite) et logo header du panneau : rond noir/blanc qui suit le theme clair/sombre (--text-primary / --bg-primary), eclair monochrome SVG a la place de l'emoji robot. Tests: - 2 nouveaux tests (429 garde / 410 retire ; repli runtime sur 410). - 278 tests passent.
This commit is contained in:
+32
-24
@@ -351,44 +351,52 @@ async def fetch_provider_models(provider: str, *, api_key: str = "",
|
||||
|
||||
|
||||
async def _validate_chat_models(base_url: str, api_key: str, candidates: list[str],
|
||||
*, timeout: float = 6.0, concurrency: int = 6,
|
||||
deadline: float = 55.0) -> list[str]:
|
||||
"""Probe `POST {base_url}/chat/completions` for each candidate with a minimal
|
||||
payload and keep the models that accept it. A 429 (rate limit) is treated as
|
||||
"probably fine" since it proves the model is routed, not that it's invalid."""
|
||||
*, timeout: float = 12.0, concurrency: int = 5,
|
||||
deadline: float = 90.0, attempts: int = 2) -> list[str]:
|
||||
"""Probe `POST {base_url}/chat/completions` for each candidate and keep only
|
||||
the models that genuinely answer — using the exact payload the app sends at
|
||||
runtime (`temperature: 0.2`, no `max_tokens`).
|
||||
|
||||
Hard failures (404 model inconnu, 410 modèle retiré…) exclude the model.
|
||||
A 429 (rate limit) is kept: it proves the route exists, so the model is
|
||||
usable once the quota frees up. Transient failures (timeouts, 5xx, network)
|
||||
are retried once before giving up, so slow-but-working models survive.
|
||||
"""
|
||||
import asyncio
|
||||
import httpx
|
||||
|
||||
valid: list[str] = []
|
||||
sem = asyncio.Semaphore(concurrency)
|
||||
start = time.monotonic()
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
payload: dict = {
|
||||
"model": None,
|
||||
url = f"{base_url.rstrip('/')}/chat/completions"
|
||||
payload_tpl = {
|
||||
"messages": [{"role": "user", "content": "ping"}],
|
||||
"max_tokens": 4,
|
||||
"temperature": 0.2,
|
||||
}
|
||||
|
||||
async def probe(model: str) -> str | None:
|
||||
if time.monotonic() - start > deadline:
|
||||
return None
|
||||
payload["model"] = model
|
||||
try:
|
||||
async with sem:
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
resp = await client.post(
|
||||
f"{base_url}/chat/completions", headers=headers, json=payload
|
||||
)
|
||||
if resp.status_code < 300 or resp.status_code == 429:
|
||||
return model
|
||||
except Exception: # noqa: BLE001 — réseau/timeout ⇒ rejeté
|
||||
return None
|
||||
payload: dict = {"model": model, **payload_tpl}
|
||||
for attempt in range(attempts):
|
||||
if time.monotonic() - start > deadline:
|
||||
return None
|
||||
try:
|
||||
async with sem:
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
resp = await client.post(url, headers=headers, json=payload)
|
||||
code = resp.status_code
|
||||
if code < 300 or code == 429:
|
||||
return model
|
||||
if code < 500: # 400/401/403/404/410… → définitivement non utilisable
|
||||
return None
|
||||
# 5xx → passage passager, on retente une fois
|
||||
except Exception: # noqa: BLE001 — timeouts / connexion → on retente
|
||||
if attempt >= attempts - 1:
|
||||
return None
|
||||
return None
|
||||
|
||||
results = await asyncio.gather(*(probe(m) for m in candidates))
|
||||
for m in results:
|
||||
if m:
|
||||
valid.append(m)
|
||||
return valid
|
||||
return [m for m in results if isinstance(m, str)]
|
||||
Reference in New Issue
Block a user