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:
@@ -1,21 +1,34 @@
|
||||
"""Curated model-capability metadata for the AI assistant.
|
||||
"""Model-capability metadata for the AI assistant.
|
||||
|
||||
ObsiGate does not query every provider for the modalities a model supports
|
||||
(not all of them expose that information, and the network call is not always
|
||||
reliable). Instead a static, curated table maps known model-name patterns to
|
||||
capability flags, with per-provider defaults. The UI uses this to show, when a
|
||||
model is selected, which features it supports (Chat, Embeddings, Rerank,
|
||||
Images, Video, Audio Speech, Audio Transcriptions, Vision).
|
||||
Two layers, in order of trust:
|
||||
|
||||
The table is intentionally conservative: an unknown model falls back to the
|
||||
provider default (usually ``chat`` only), so we never claim a capability the
|
||||
model may not have.
|
||||
1. **Provider-declared** (:mod:`backend.provider_capabilities`) — when the
|
||||
provider publishes per-model capabilities in its models endpoint (Mistral
|
||||
``capabilities``, OpenRouter ``architecture``), that declaration *wins* for
|
||||
every flag it mentions. The snapshot is cached by the model-list endpoint.
|
||||
2. **Curated table** (this module) — a static, hand-maintained map of known
|
||||
model-name patterns to capability flags with per-provider defaults. It fills
|
||||
the flags the provider stays silent about, and is the only source for
|
||||
providers and models that declare nothing (offline, no API key, DeepSeek,
|
||||
NVIDIA, QwenCloud, Xiaomi…).
|
||||
|
||||
The UI uses the result to show, when a model is selected, which features it
|
||||
supports (Chat, Embeddings, Rerank, Images, Video, Audio Speech, Audio
|
||||
Transcriptions, Vision).
|
||||
|
||||
The curated layer is intentionally conservative: an unknown model falls back to
|
||||
the provider default (usually ``chat`` only), so we never claim a capability the
|
||||
model may not have. A provider declaration, on the other hand, is authoritative
|
||||
in both directions — it can also *revoke* a flag the curated table guessed
|
||||
wrongly (e.g. ``mistral-embed`` declares no chat).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from backend.provider_capabilities import get_declared_capabilities
|
||||
|
||||
# Ordered list of capability keys exposed to the UI. Keep in sync with the
|
||||
# frontend ``AI_CAPABILITY_KEYS`` and the i18n ``ai.cap_*`` labels.
|
||||
CAPABILITY_KEYS: tuple[str, ...] = (
|
||||
@@ -46,7 +59,10 @@ _PROVIDER_DEFAULTS: dict[str, dict[str, bool]] = {
|
||||
"nvidia": _caps(chat=True),
|
||||
"qwencloud": _caps(chat=True),
|
||||
"xiaomi": _caps(chat=True),
|
||||
"mistral": _caps(chat=True, embeddings=True),
|
||||
# Mistral: chat only. ``embeddings`` used to be assumed for every Mistral
|
||||
# model, which wrongly labelled mistral-large / codestral as embedders
|
||||
# (BUG-044); the ``embed`` rule below covers the real embedding models.
|
||||
"mistral": _caps(chat=True),
|
||||
}
|
||||
|
||||
# Ordered (substrings, capabilities) rules — the first matching rule wins.
|
||||
@@ -76,6 +92,8 @@ _MODEL_RULES: list[tuple[tuple[str, ...], dict[str, bool]]] = [
|
||||
),
|
||||
# Video generation.
|
||||
(("veo-", "sora", "video-gen", "-video"), _caps(video=True)),
|
||||
# Document OCR models — image input, but not a chat endpoint.
|
||||
(("mistral-ocr",), _caps(vision=True)),
|
||||
# Vision-capable chat models (multimodal input).
|
||||
(
|
||||
(
|
||||
@@ -97,15 +115,36 @@ _MODEL_RULES: list[tuple[tuple[str, ...], dict[str, bool]]] = [
|
||||
"minicpm-v",
|
||||
"llama-3.2-vision",
|
||||
"mimo-vl",
|
||||
# Mistral vision families (BUG-044): text-only mistral-large,
|
||||
# codestral and voxtral are deliberately absent.
|
||||
"ministral",
|
||||
"magistral",
|
||||
"mistral-small",
|
||||
"mistral-medium",
|
||||
"mistral-vibe-cli",
|
||||
"labs-leanstral",
|
||||
),
|
||||
_caps(chat=True, vision=True),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _curated_capabilities(provider: str, model: str) -> dict[str, bool]:
|
||||
"""Curated table lookup (model rules first, then the provider default)."""
|
||||
if model:
|
||||
for needles, caps in _MODEL_RULES:
|
||||
if any(needle in model for needle in needles):
|
||||
return dict(caps)
|
||||
return dict(_PROVIDER_DEFAULTS.get(provider, _caps(chat=True)))
|
||||
|
||||
|
||||
def get_model_capabilities(provider: str, model: str) -> dict[str, bool]:
|
||||
"""Return the capability flags for a ``provider``/``model`` pair.
|
||||
|
||||
A provider declaration (see :mod:`backend.provider_capabilities`) overrides
|
||||
the curated table for every flag it mentions; the curated table supplies the
|
||||
rest.
|
||||
|
||||
Args:
|
||||
provider: Provider identifier (e.g. ``"deepseek"``). Case-insensitive.
|
||||
model: Model identifier (e.g. ``"deepseek-chat"``). May be empty, in
|
||||
@@ -116,11 +155,11 @@ def get_model_capabilities(provider: str, model: str) -> dict[str, bool]:
|
||||
"""
|
||||
provider = (provider or "").strip().lower()
|
||||
name = (model or "").strip().lower()
|
||||
if name:
|
||||
for needles, caps in _MODEL_RULES:
|
||||
if any(needle in name for needle in needles):
|
||||
return dict(caps)
|
||||
return dict(_PROVIDER_DEFAULTS.get(provider, _caps(chat=True)))
|
||||
caps = _curated_capabilities(provider, name)
|
||||
declared = get_declared_capabilities(provider, name)
|
||||
if declared:
|
||||
caps.update({key: value for key, value in declared.items() if key in CAPABILITY_KEYS})
|
||||
return caps
|
||||
|
||||
|
||||
def get_capabilities_for_models(
|
||||
|
||||
Reference in New Issue
Block a user