fix(ai): BUG-044 capacités des modèles lues chez le fournisseur (Mistral vision)
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:
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user