From 39d34ac866e10b092246d1b5774c05dd7cbc4b74 Mon Sep 17 00:00:00 2001 From: bruno Date: Wed, 7 Oct 2026 16:25:04 -0400 Subject: [PATCH] =?UTF-8?q?fix(llm):=20Mistral=20403=20tier=5Fnot=5Fallowe?= =?UTF-8?q?d=20=E2=80=94=20d=C3=A9fauts=20tous-plans=20+=20sonde=20chat=20?= =?UTF-8?q?+=20migration=2036=20(v7.59.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La clé fonctionne: /v1/models 200 + chat 200 (vérifié live). L'échec venait du modèle: mistral-large-latest (défaut PROVIDERS + tête de liste) n'est pas servi par les plans d'abonnement basiques → 403 code 1910 « tier_not_allowed », et le Test connection sélectionnait systématiquement ce premier candidat. - PROVIDERS mistral default → mistral-small-latest (tous plans) - PROVIDER_MODELS réordonnée: tous-plans d'abord (ordre du test de connexion) - mistral ∈ _CHAT_VALIDATED_PROVIDERS: le fetch modèles ne garde que les modèles réellement servis (46 listés → 25 utilisables avec la clé du compte) - migration 36: default_model bloqué (large/pixtral-large) reset en base Test live déployé: {ok:true, model:mistral-small-latest, reply:PONG, verified:true} · tests/test_agent.py 53/53 · nouveau test d'ancrage test_mistral_defaults_are_tier_safe --- CHANGELOG.md | 24 ++++++++++++++++++++++++ VERSION | 2 +- app/main.py | 2 +- app/migrations.py | 21 +++++++++++++++++++++ app/services/llm_client.py | 11 ++++++++--- app/services/llm_config.py | 5 ++++- tests/test_agent.py | 15 +++++++++++++++ 7 files changed, 74 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a74cd..c0b6d91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog - FlowDeck +## v7.59.1 (2026-10-07) — Fix fournisseur Mistral (clé valide, 403 tier_not_allowed) + +### Fixed + +- **Mistral « Test connection » échouait malgré une clé valide** — cause + racine : `mistral-large-latest` (modèle par défaut + tête de liste) n'est + pas servi par les plans d'abonnement basiques → `403 tier_not_allowed` + (code 1910). La clé elle-même fonctionne (`/v1/models` 200, chat 200 sur + small/medium/nemo/codestral — vérifié en direct). + - `PROVIDERS["mistral"]` default → `mistral-small-latest` (tous plans). + - `PROVIDER_MODELS["mistral"]` réordonnée : modèles tous-plans d'abord + (le test de connexion sélectionne le premier candidat). + - `mistral` ajouté à `_CHAT_VALIDATED_PROVIDERS` : le fetch des modèles + sonde chat/completions et exclut les modèles hors plan (46 listés → + 25 réellement utilisables avec la clé du compte). + - **Migration 36** : `default_model` des lignes mistral pointant sur un + modèle bloqué réinitialisé (→ nouveau défaut), `llm_config.model` idem. + +### Tests + +- `tests/test_agent.py::test_mistral_defaults_are_tier_safe` · suite agent + OK · test de connexion live sur l'instance déployée : `{"ok":true, + "model":"mistral-small-latest","reply":"PONG","verified":true}`. + ## v7.59.0 (2026-10-07) — Mobile : drawer réglable, side-nav settings, aide refondue ### Fixed diff --git a/VERSION b/VERSION index 5ce8bfd..b50130e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.59.0 +7.59.1 diff --git a/app/main.py b/app/main.py index a0f5413..1d69fe4 100644 --- a/app/main.py +++ b/app/main.py @@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.59.0", + version="7.59.1", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/app/migrations.py b/app/migrations.py index 6712fae..ec6e96f 100644 --- a/app/migrations.py +++ b/app/migrations.py @@ -1678,3 +1678,24 @@ def _migration_agent_memory(conn: sqlite3.Connection) -> None: updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )""" ) + + +@register(36, "v7.59.1: mistral — modèles par défaut hors plan basique") +def _migration_mistral_default_model(conn: sqlite3.Connection) -> None: + """mistral-large-latest / pixtral-large-latest renvoient 403 + « tier_not_allowed » sur les plans d'abonnement basiques : la clé est + valide mais le test de connexion et l'agent échouaient. Reset du + default_model stocké → vide = nouveau défaut du provider (small-latest, + servi par tous les plans). + """ + blocked = ("mistral-large-latest", "pixtral-large-latest") + conn.execute( + "UPDATE user_llm_keys SET default_model='' " + "WHERE provider='mistral' AND default_model IN (?,?)", + blocked, + ) + conn.execute( + "UPDATE llm_config SET model='mistral-small-latest' " + "WHERE provider='mistral' AND model IN (?,?)", + blocked, + ) diff --git a/app/services/llm_client.py b/app/services/llm_client.py index 466840d..a6c71a9 100644 --- a/app/services/llm_client.py +++ b/app/services/llm_client.py @@ -37,7 +37,9 @@ logger = logging.getLogger(__name__) PROVIDERS = { "openai": ("https://api.openai.com/v1", "gpt-4o"), "anthropic": ("https://api.anthropic.com/v1", "claude-opus-4-8"), - "mistral": ("https://api.mistral.ai/v1", "mistral-large-latest"), + # mistral-small est servi par tous les plans d'abonnement ; + # mistral-large → 403 tier_not_allowed sur les plans basiques. + "mistral": ("https://api.mistral.ai/v1", "mistral-small-latest"), "cohere": ("https://api.cohere.ai/compatibility/v1", "command-a-plus-05-2026"), "google": ("https://generativelanguage.googleapis.com/v1beta/openai", "gemini-2.0-flash"), "groq": ("https://api.groq.com/openai/v1", "llama-3.3-70b-versatile"), @@ -94,8 +96,11 @@ PROVIDER_LABELS: dict[str, str] = { PROVIDER_MODELS: dict[str, list[str]] = { "openai": ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini", "o3-mini", "gpt-4-turbo"], "anthropic": ["claude-opus-4-8", "claude-sonnet-4-5", "claude-3-5-sonnet", "claude-haiku-4-5"], - "mistral": ["mistral-large-latest", "mistral-medium-latest", "mistral-small-latest", - "codestral-latest", "open-mistral-nemo", "pixtral-large-latest"], + # Ordre = ordre de sélection du test de connexion : les modèles servis par + # tous les plans d'abord (large/pixtral-large = 403 tier_not_allowed en plan + # basique), les modèles à plan élevé en fin de liste. + "mistral": ["mistral-small-latest", "mistral-medium-latest", "open-mistral-nemo", + "codestral-latest", "mistral-large-latest", "pixtral-large-latest"], "cohere": ["command-a-plus-05-2026", "command-r-plus", "command-r", "command-a-03-2025"], "google": ["gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-1.5-pro", "gemini-1.5-flash"], "groq": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", diff --git a/app/services/llm_config.py b/app/services/llm_config.py index df94c83..3882c8d 100644 --- a/app/services/llm_config.py +++ b/app/services/llm_config.py @@ -22,7 +22,10 @@ from app.services.llm_client import PROVIDER_LABELS, PROVIDER_MODELS, PROVIDERS # before being exposed as "usable models". NVIDIA exposes all of its catalog # (embeddings, rerank, image/video/audio gen…) many of which answer 404 on # chat completions — the exact failure the user hit. -_CHAT_VALIDATED_PROVIDERS = frozenset({"nvidia"}) +# Mistral : le /models liste des modèles hors du plan d'abonnement du compte +# (403 « tier_not_allowed » ex. mistral-large sur plan basique) — la sonde +# chat ne garde que ceux réellement servis par la CLÉ de l'utilisateur. +_CHAT_VALIDATED_PROVIDERS = frozenset({"nvidia", "mistral"}) # Markers that identify clearly non-chat models (embeddings, rerank, media gen…). _NON_CHAT_MARKERS = ( diff --git a/tests/test_agent.py b/tests/test_agent.py index 1733dd0..5173a88 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -89,6 +89,21 @@ def test_llm_client_providers_listed(client): assert name in PROVIDERS +def test_mistral_defaults_are_tier_safe(client): + """La clé Mistral plan basique → 403 « tier_not_allowed » sur mistral-large/pixtral-large. + Le modèle par défaut et le premier de la liste du test de connexion doivent + être des modèles servis par tous les plans.""" + from app.services.llm_client import PROVIDERS, PROVIDER_MODELS + tier_blocked = {"mistral-large-latest", "pixtral-large-latest"} + assert PROVIDERS["mistral"][1] not in tier_blocked + assert PROVIDER_MODELS["mistral"][0] not in tier_blocked + # le fallback du modèle retiré (PROVIDERS default) est présent dans la liste + assert PROVIDERS["mistral"][1] in PROVIDER_MODELS["mistral"] + # et la liste des modèles Mistral est validée chat à chaque fetch (403 exclus) + from app.services.llm_config import _CHAT_VALIDATED_PROVIDERS + assert "mistral" in _CHAT_VALIDATED_PROVIDERS + + def test_llm_client_openai_compatible_bases(client): """Providers that need an OpenAI-compatible surface must point at it.