fix(ai): BUG-044 capacités des modèles lues chez le fournisseur (Mistral vision)
CI / lint (push) Successful in 1m20s
CI / security (push) Successful in 53s
CI / test (push) Successful in 2m27s
CI / build (push) Successful in 50s
CI / e2e (push) Successful in 10m48s

Le panneau de modèle par défaut et la bulle ⓘ n'affichaient aucun modèle Mistral
« Vision capable » alors que GET api.mistral.ai/v1/models en déclare 28 : la table
de capacités était entièrement statique et aucun de ses motifs ne correspondait aux
familles Mistral actuelles (seul `pixtral`, retiré de l'API, les matchait).

- backend/provider_capabilities.py (nouveau) : capacités déclarées par le
  fournisseur (Mistral `capabilities`, OpenRouter `architecture`), détectées par
  la forme du payload, snapshot en cache process-wide (TTL 30 min, surchargeable
  par AI_CAPABILITIES_TTL_SECONDS) rempli par GET /api/config/ai-models.
- backend/model_capabilities.py : une déclaration prime sur la table statique
  pour chaque drapeau mentionné ; la table ne comble que le reste (Mistral ne
  déclare jamais `embedding`). Table corrigée pour le repli hors ligne : familles
  vision Mistral (ministral, magistral, mistral-small, mistral-medium,
  mistral-vibe-cli, labs-leanstral), mistral-ocr = vision sans chat, et défaut du
  fournisseur Mistral sans `embeddings` (mistral-large / codestral n'étaient plus
  des « embedders »).
- backend/ai_routes.py : GET /api/ai/model-capabilities reste sans appel réseau
  (cache froid → table statique).
- Tests : tests/test_provider_capabilities.py (nouveau), TestMistralFamilies et
  TestDeclaredCapabilities (bout en bout via l'API).
- Docs : CHANGELOG [Unreleased], registre + journal ISSUES_TODOLIST,
  fiche docs/features/ai-provider-picker.md (§L).

Vérifié : 28/28 modèles vision déclarés par Mistral détectés (0 avant), 0 écart
dans les deux sens ; pytest 1007 passed / 6 skipped ; ruff 0 (backend) ; mypy 0 ;
tests frontend unit 9/9 + IA 66/66 + validate-imports 37 modules ; instance de test
reconstruite et vérifiée sur http://localhost:2020.
This commit is contained in:
2026-09-15 09:26:31 -04:00
parent 140a8f6efe
commit b69cb9b0f8
10 changed files with 723 additions and 23 deletions
+94
View File
@@ -2,6 +2,8 @@
from __future__ import annotations
import pytest
from backend.model_capabilities import (
CAPABILITY_KEYS,
get_capabilities_for_models,
@@ -10,6 +12,16 @@ from backend.model_capabilities import (
)
@pytest.fixture(autouse=True)
def _no_declared_caps():
"""Curated-table tests must not see the process-wide declaration cache."""
from backend.provider_capabilities import clear_declared_capabilities
clear_declared_capabilities()
yield
clear_declared_capabilities()
class TestCapabilityShape:
def test_every_result_has_all_keys(self):
for provider, model in [
@@ -84,3 +96,85 @@ class TestBatch:
)
assert result["qwen-max"]["vision"] is False
assert result["qwen-vl-max"]["vision"] is True
class TestMistralFamilies:
"""BUG-044 — the curated fallback must know the current Mistral families.
This fallback is what answers when the provider declaration is unavailable
(no API key, offline, cold cache), so it has to agree with what
``api.mistral.ai/v1/models`` declares today. Before the fix, no Mistral
model at all was reported as vision-capable — only the dead ``pixtral``
pattern matched.
"""
@pytest.mark.parametrize(
"model",
[
"mistral-small-latest",
"mistral-small-2603",
"mistral-medium-latest",
"mistral-medium-3.5",
"ministral-3b-latest",
"ministral-8b-2512",
"ministral-14b-latest",
"magistral-small-latest",
"magistral-medium-latest",
"mistral-vibe-cli-latest",
"pixtral-12b-2409",
],
)
def test_vision_chat_families(self, model):
caps = get_model_capabilities("mistral", model)
assert caps["vision"] is True, model
assert caps["chat"] is True, model
@pytest.mark.parametrize(
"model",
[
"mistral-large-latest",
"codestral-latest",
"open-mixtral-8x7b",
"voxtral-small-latest",
"mistral-code-latest",
],
)
def test_text_only_models_are_not_vision(self, model):
caps = get_model_capabilities("mistral", model)
assert caps["vision"] is False, model
assert caps["chat"] is True, model
def test_ocr_models_are_vision_without_chat(self):
caps = get_model_capabilities("mistral", "mistral-ocr-latest")
assert caps["vision"] is True
assert caps["chat"] is False
def test_embeddings_only_for_the_embed_models(self):
assert get_model_capabilities("mistral", "mistral-embed")["embeddings"] is True
# Regression: every Mistral model used to inherit embeddings=True from
# the provider default, wrongly branding chat models as embedders.
assert get_model_capabilities("mistral", "mistral-large-latest")["embeddings"] is False
assert get_model_capabilities("mistral", "some-unknown-mistral")["embeddings"] is False
def test_current_api_vision_models_are_all_covered(self):
"""Every model the API declares vision=true is vision-capable offline.
List captured from ``GET https://api.mistral.ai/v1/models`` (2026-09-15);
the live declaration layer covers renames, this guards the offline path.
"""
api_vision_models = [
"magistral-medium-latest", "magistral-small-latest",
"ministral-14b-2512", "ministral-14b-latest",
"ministral-3b-2512", "ministral-3b-latest",
"ministral-8b-2512", "ministral-8b-latest",
"mistral-medium", "mistral-medium-2604", "mistral-medium-3",
"mistral-medium-3-5", "mistral-medium-3.5", "mistral-medium-latest",
"mistral-ocr-2512", "mistral-ocr-3", "mistral-ocr-3-0",
"mistral-ocr-4", "mistral-ocr-4-0", "mistral-ocr-4-1",
"mistral-ocr-latest",
"mistral-small-2603", "mistral-small-latest",
"mistral-vibe-cli-fast", "mistral-vibe-cli-latest",
"mistral-vibe-cli-with-tools",
]
missing = [m for m in api_vision_models if not model_supports_vision("mistral", m)]
assert missing == [], f"curated fallback misses vision for: {missing}"