Claude commited on
Commit
d82da85
·
unverified ·
1 Parent(s): 9448ccc

refactor: use API-reported supports_vision instead of hardcoded model names

Browse files

The vision capability of AI models is now determined entirely by the
provider API (e.g. Mistral capabilities.vision) and persisted in the
database, rather than guessing from model name substrings.

Changes:
- ModelConfigDB: add supports_vision column (Boolean)
- PUT /corpora/{id}/model: accept and persist supports_vision from client
- generate_content(): add supports_vision param, passed from DB through
ModelConfig → analyzer → provider, no more name-based heuristics
- provider_mistral._model_supports_vision(): use only capabilities.vision
from the API response, return False without model object (safe default)
- job_runner: read supports_vision from DB instead of hardcoding True
- Frontend: send supports_vision from model list when selecting a model

https://claude.ai/code/session_01Sxmf2zTTcjaQeZXCeXaCxr

backend/app/api/v1/models_api.py CHANGED
@@ -45,6 +45,7 @@ class ModelSelectRequest(BaseModel):
45
  model_id: str = Field(..., min_length=1, max_length=256)
46
  provider_type: str = Field(..., min_length=1, max_length=64)
47
  display_name: str = Field("", max_length=256)
 
48
 
49
 
50
  class ModelConfigResponse(BaseModel):
@@ -54,6 +55,7 @@ class ModelConfigResponse(BaseModel):
54
  provider_type: str
55
  selected_model_id: str
56
  selected_model_display_name: str
 
57
  updated_at: datetime
58
 
59
 
@@ -133,6 +135,7 @@ async def set_corpus_model(
133
  provider_type=body.provider_type,
134
  selected_model_id=body.model_id,
135
  selected_model_display_name=display_name,
 
136
  updated_at=datetime.now(timezone.utc),
137
  )
138
  db.add(config)
@@ -140,6 +143,7 @@ async def set_corpus_model(
140
  config.provider_type = body.provider_type
141
  config.selected_model_id = body.model_id
142
  config.selected_model_display_name = display_name
 
143
  config.updated_at = datetime.now(timezone.utc)
144
 
145
  await db.commit()
 
45
  model_id: str = Field(..., min_length=1, max_length=256)
46
  provider_type: str = Field(..., min_length=1, max_length=64)
47
  display_name: str = Field("", max_length=256)
48
+ supports_vision: bool = Field(True)
49
 
50
 
51
  class ModelConfigResponse(BaseModel):
 
55
  provider_type: str
56
  selected_model_id: str
57
  selected_model_display_name: str
58
+ supports_vision: bool
59
  updated_at: datetime
60
 
61
 
 
135
  provider_type=body.provider_type,
136
  selected_model_id=body.model_id,
137
  selected_model_display_name=display_name,
138
+ supports_vision=body.supports_vision,
139
  updated_at=datetime.now(timezone.utc),
140
  )
141
  db.add(config)
 
143
  config.provider_type = body.provider_type
144
  config.selected_model_id = body.model_id
145
  config.selected_model_display_name = display_name
146
+ config.supports_vision = body.supports_vision
147
  config.updated_at = datetime.now(timezone.utc)
148
 
149
  await db.commit()
backend/app/models/model_config_db.py CHANGED
@@ -8,7 +8,7 @@ La clé API n'est JAMAIS stockée ici (R06) — elle reste dans l'environnement.
8
  from datetime import datetime
9
 
10
  # 2. third-party
11
- from sqlalchemy import DateTime, ForeignKey, String
12
  from sqlalchemy.orm import Mapped, mapped_column
13
 
14
  # 3. local
@@ -26,4 +26,5 @@ class ModelConfigDB(Base):
26
  provider_type: Mapped[str] = mapped_column(String, nullable=False)
27
  selected_model_id: Mapped[str] = mapped_column(String, nullable=False)
28
  selected_model_display_name: Mapped[str] = mapped_column(String, nullable=False)
 
29
  updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
 
8
  from datetime import datetime
9
 
10
  # 2. third-party
11
+ from sqlalchemy import Boolean, DateTime, ForeignKey, String
12
  from sqlalchemy.orm import Mapped, mapped_column
13
 
14
  # 3. local
 
26
  provider_type: Mapped[str] = mapped_column(String, nullable=False)
27
  selected_model_id: Mapped[str] = mapped_column(String, nullable=False)
28
  selected_model_display_name: Mapped[str] = mapped_column(String, nullable=False)
29
+ supports_vision: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
30
  updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
backend/app/services/ai/analyzer.py CHANGED
@@ -121,6 +121,7 @@ def run_primary_analysis(
121
  image_bytes=jpeg_bytes,
122
  prompt=prompt_text,
123
  model_id=model_config.selected_model_id,
 
124
  )
125
 
126
  # ── 4. Écriture ai_raw.json TOUJOURS EN PREMIER (R05) ─────────────────
 
121
  image_bytes=jpeg_bytes,
122
  prompt=prompt_text,
123
  model_id=model_config.selected_model_id,
124
+ supports_vision=model_config.supports_vision,
125
  )
126
 
127
  # ── 4. Écriture ai_raw.json TOUJOURS EN PREMIER (R05) ─────────────────
backend/app/services/ai/base.py CHANGED
@@ -42,13 +42,21 @@ class AIProvider(ABC):
42
  ...
43
 
44
  @abstractmethod
45
- def generate_content(self, image_bytes: bytes, prompt: str, model_id: str) -> str:
 
 
 
 
 
 
46
  """Envoie une image + prompt à l'IA et retourne le texte brut de la réponse.
47
 
48
  Args:
49
  image_bytes: contenu JPEG de l'image dérivée.
50
  prompt: texte du prompt rendu depuis le template.
51
  model_id: identifiant technique du modèle à utiliser.
 
 
52
 
53
  Returns:
54
  Texte brut retourné par l'API (avant parsing).
 
42
  ...
43
 
44
  @abstractmethod
45
+ def generate_content(
46
+ self,
47
+ image_bytes: bytes,
48
+ prompt: str,
49
+ model_id: str,
50
+ supports_vision: bool = True,
51
+ ) -> str:
52
  """Envoie une image + prompt à l'IA et retourne le texte brut de la réponse.
53
 
54
  Args:
55
  image_bytes: contenu JPEG de l'image dérivée.
56
  prompt: texte du prompt rendu depuis le template.
57
  model_id: identifiant technique du modèle à utiliser.
58
+ supports_vision: True si le modèle accepte les images (déterminé
59
+ par l'API du provider lors du listing, stocké en BDD).
60
 
61
  Returns:
62
  Texte brut retourné par l'API (avant parsing).
backend/app/services/ai/provider_google_ai.py CHANGED
@@ -55,7 +55,7 @@ class GoogleAIProvider(AIProvider):
55
  )
56
  return result
57
 
58
- def generate_content(self, image_bytes: bytes, prompt: str, model_id: str) -> str:
59
  if not self.is_configured():
60
  raise RuntimeError(f"Variable d'environnement manquante : {_ENV_KEY}")
61
  client = genai.Client(api_key=os.environ[_ENV_KEY])
 
55
  )
56
  return result
57
 
58
+ def generate_content(self, image_bytes: bytes, prompt: str, model_id: str, supports_vision: bool = True) -> str:
59
  if not self.is_configured():
60
  raise RuntimeError(f"Variable d'environnement manquante : {_ENV_KEY}")
61
  client = genai.Client(api_key=os.environ[_ENV_KEY])
backend/app/services/ai/provider_mistral.py CHANGED
@@ -73,25 +73,21 @@ def _is_ocr_model(model_id: str) -> bool:
73
 
74
 
75
 
76
- # Modèles Mistral connus pour supporter la vision (multimodaux).
77
- # Mistral Small 25.01+ et Mistral Medium sont multimodaux.
78
- # Cette liste sert de fallback quand capabilities.vision n'est pas exposé par le SDK.
79
- _VISION_MODEL_SUBSTRINGS = ("pixtral", "vision", "ocr", "mistral-small", "mistral-medium")
80
-
81
-
82
  def _model_supports_vision(model_id: str, model_obj: object = None) -> bool:
83
  """Détecte si un modèle Mistral supporte les entrées image.
84
 
85
- Utilise capabilities.vision si disponible (objet SDK v1.x),
86
- sinon se rabat sur la présence de sous-chaînes connues dans l'ID du modèle.
87
- Mistral Small (25.01+) et Mistral Medium sont multimodaux.
 
88
  """
89
  if model_obj is not None:
90
  caps = getattr(model_obj, "capabilities", None)
91
  if caps is not None:
92
  return bool(getattr(caps, "vision", False))
93
- mid = model_id.lower()
94
- return any(sub in mid for sub in _VISION_MODEL_SUBSTRINGS)
 
95
 
96
 
97
  class MistralProvider(AIProvider):
@@ -188,18 +184,25 @@ class MistralProvider(AIProvider):
188
  )
189
  return list(_MISTRAL_FALLBACK_MODELS)
190
 
191
- def generate_content(self, image_bytes: bytes, prompt: str, model_id: str) -> str:
 
 
 
 
 
 
192
  """Envoie image + prompt à Mistral et retourne le texte brut.
193
 
194
  Trois chemins selon le modèle :
195
  1. OCR (mistral-ocr-latest) :
196
  client.ocr.process() → markdown de toutes les pages concaténées.
197
- L'endpoint OCR retourne du texte structuré, pas des messages chat.
198
- 2. Vision (Pixtral) :
199
  client.chat.complete() avec content multimodal (image base64 + texte).
200
- 3. Texte seul (Mistral Large, Small, Codestral) :
201
  client.chat.complete() avec prompt texte uniquement.
202
- L'image n'est pas transmise (avertissement loggé).
 
 
203
  """
204
  if not self.is_configured():
205
  raise RuntimeError(
@@ -230,8 +233,8 @@ class MistralProvider(AIProvider):
230
  getattr(page, "markdown", "") for page in pages
231
  )
232
 
233
- # ── Chemin 2 : Vision multimodale (Pixtral) ──────────────────────────
234
- if _model_supports_vision(model_id):
235
  content: object = [
236
  {"type": "image_url", "image_url": {"url": data_url}},
237
  {"type": "text", "text": prompt},
 
73
 
74
 
75
 
 
 
 
 
 
 
76
  def _model_supports_vision(model_id: str, model_obj: object = None) -> bool:
77
  """Détecte si un modèle Mistral supporte les entrées image.
78
 
79
+ Source de vérité unique : capabilities.vision retourné par l'API Mistral.
80
+ Aucune liste hardcodée de noms de modèles l'API fait autorité.
81
+ Si capabilities n'est pas disponible (SDK ancien), retourne False
82
+ par sécurité (le modèle sera utilisé en mode texte seul).
83
  """
84
  if model_obj is not None:
85
  caps = getattr(model_obj, "capabilities", None)
86
  if caps is not None:
87
  return bool(getattr(caps, "vision", False))
88
+ # Sans objet modèle (fallback statique), on ne peut pas deviner :
89
+ # retourner False pour éviter d'envoyer une image à un modèle texte seul.
90
+ return False
91
 
92
 
93
  class MistralProvider(AIProvider):
 
184
  )
185
  return list(_MISTRAL_FALLBACK_MODELS)
186
 
187
+ def generate_content(
188
+ self,
189
+ image_bytes: bytes,
190
+ prompt: str,
191
+ model_id: str,
192
+ supports_vision: bool = True,
193
+ ) -> str:
194
  """Envoie image + prompt à Mistral et retourne le texte brut.
195
 
196
  Trois chemins selon le modèle :
197
  1. OCR (mistral-ocr-latest) :
198
  client.ocr.process() → markdown de toutes les pages concaténées.
199
+ 2. Vision (supports_vision=True) :
 
200
  client.chat.complete() avec content multimodal (image base64 + texte).
201
+ 3. Texte seul (supports_vision=False) :
202
  client.chat.complete() avec prompt texte uniquement.
203
+
204
+ Le flag supports_vision est déterminé dynamiquement par l'API Mistral
205
+ lors du listing des modèles (capabilities.vision), puis stocké en BDD.
206
  """
207
  if not self.is_configured():
208
  raise RuntimeError(
 
233
  getattr(page, "markdown", "") for page in pages
234
  )
235
 
236
+ # ── Chemin 2 : Vision multimodale ────────────────────────────────────
237
+ if supports_vision:
238
  content: object = [
239
  {"type": "image_url", "image_url": {"url": data_url}},
240
  {"type": "text", "text": prompt},
backend/app/services/ai/provider_vertex_key.py CHANGED
@@ -57,5 +57,5 @@ class VertexAPIKeyProvider(AIProvider):
57
  def list_models(self) -> list[ModelInfo]:
58
  raise RuntimeError(_UNAVAILABLE_MSG)
59
 
60
- def generate_content(self, image_bytes: bytes, prompt: str, model_id: str) -> str:
61
  raise RuntimeError(_UNAVAILABLE_MSG)
 
57
  def list_models(self) -> list[ModelInfo]:
58
  raise RuntimeError(_UNAVAILABLE_MSG)
59
 
60
+ def generate_content(self, image_bytes: bytes, prompt: str, model_id: str, supports_vision: bool = True) -> str:
61
  raise RuntimeError(_UNAVAILABLE_MSG)
backend/app/services/ai/provider_vertex_sa.py CHANGED
@@ -85,7 +85,7 @@ class VertexServiceAccountProvider(AIProvider):
85
  )
86
  return result
87
 
88
- def generate_content(self, image_bytes: bytes, prompt: str, model_id: str) -> str:
89
  if not self.is_configured():
90
  raise RuntimeError(f"Variable d'environnement manquante : {_ENV_KEY}")
91
  client = self._build_client()
 
85
  )
86
  return result
87
 
88
+ def generate_content(self, image_bytes: bytes, prompt: str, model_id: str, supports_vision: bool = True) -> str:
89
  if not self.is_configured():
90
  raise RuntimeError(f"Variable d'environnement manquante : {_ENV_KEY}")
91
  client = self._build_client()
backend/app/services/job_runner.py CHANGED
@@ -126,7 +126,7 @@ async def _run_job_impl(job_id: str, db: AsyncSession) -> None:
126
  selected_model_id=model_db.selected_model_id,
127
  selected_model_display_name=model_db.selected_model_display_name,
128
  provider=ProviderType(model_db.provider_type),
129
- supports_vision=True,
130
  last_fetched_at=model_db.updated_at,
131
  available_models=[],
132
  )
 
126
  selected_model_id=model_db.selected_model_id,
127
  selected_model_display_name=model_db.selected_model_display_name,
128
  provider=ProviderType(model_db.provider_type),
129
+ supports_vision=model_db.supports_vision,
130
  last_fetched_at=model_db.updated_at,
131
  available_models=[],
132
  )
backend/tests/test_provider_mistral.py CHANGED
@@ -88,39 +88,36 @@ def _make_fake_mistralai(models: list[_FakeModel] | None = None) -> _types.Modul
88
 
89
 
90
  # ---------------------------------------------------------------------------
91
- # _model_supports_vision() — helper pur
92
  # ---------------------------------------------------------------------------
93
 
94
- def test_vision_detection_pixtral_by_name():
95
- assert _model_supports_vision("pixtral-large-latest") is True
96
- assert _model_supports_vision("pixtral-12b-2409") is True
97
-
98
-
99
- def test_vision_detection_multimodal_models_by_name():
100
- """Mistral Small 25.01+ et Mistral Medium sont multimodaux."""
101
- assert _model_supports_vision("mistral-small-latest") is True
102
- assert _model_supports_vision("mistral-small-2501") is True
103
- assert _model_supports_vision("mistral-medium-latest") is True
104
-
105
-
106
- def test_vision_detection_text_models_by_name():
107
- assert _model_supports_vision("mistral-large-latest") is False
108
  assert _model_supports_vision("codestral-latest") is False
109
 
110
 
111
- def test_vision_detection_uses_capabilities_when_available():
 
112
  m_vision = _FakeModel("some-model", vision=True)
113
  m_text = _FakeModel("some-model", vision=False)
114
  assert _model_supports_vision("some-model", m_vision) is True
115
  assert _model_supports_vision("some-model", m_text) is False
116
 
117
 
118
- def test_vision_detection_capabilities_override_name():
119
- """capabilities.vision=False surpasse un nom contenant 'pixtral'."""
120
  m = _FakeModel("pixtral-test", vision=False)
121
  assert _model_supports_vision("pixtral-test", m) is False
122
 
123
 
 
 
 
 
 
 
124
  # ---------------------------------------------------------------------------
125
  # is_configured()
126
  # ---------------------------------------------------------------------------
@@ -269,25 +266,27 @@ def test_list_models_fallback_backward_compat():
269
  # ---------------------------------------------------------------------------
270
 
271
  def test_generate_content_vision_model_returns_text(monkeypatch):
272
- """Modèle vision (Pixtral) : envoie l'image et retourne la réponse."""
273
  monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
274
  fake = _make_fake_mistralai()
275
  monkeypatch.setitem(sys.modules, "mistralai", fake)
276
 
277
  result = MistralProvider().generate_content(
278
- b"fake-jpeg", "Analyse ce folio.", "pixtral-large-latest"
 
279
  )
280
  assert result == "Voici le JSON de la page."
281
 
282
 
283
  def test_generate_content_text_model_returns_text(monkeypatch):
284
- """Modèle texte (Mistral Large) : envoie seulement le prompt, retourne la réponse."""
285
  monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
286
  fake = _make_fake_mistralai()
287
  monkeypatch.setitem(sys.modules, "mistralai", fake)
288
 
289
  result = MistralProvider().generate_content(
290
- b"fake-jpeg", "Analyse ce folio.", "mistral-large-latest"
 
291
  )
292
  assert result == "Voici le JSON de la page."
293
 
@@ -311,7 +310,7 @@ def test_generate_content_vision_sends_image_url(monkeypatch):
311
  fake.Mistral = _FakeMistral
312
  monkeypatch.setitem(sys.modules, "mistralai", fake)
313
 
314
- MistralProvider().generate_content(b"jpeg", "prompt", "pixtral-large-latest")
315
 
316
  assert len(captured) == 1
317
  content = captured[0]["content"]
@@ -322,7 +321,7 @@ def test_generate_content_vision_sends_image_url(monkeypatch):
322
 
323
 
324
  def test_generate_content_text_sends_string_content(monkeypatch):
325
- """Modèle texte : le message content est une chaîne (pas d'image)."""
326
  monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
327
  captured: list[dict] = []
328
 
@@ -340,7 +339,7 @@ def test_generate_content_text_sends_string_content(monkeypatch):
340
  fake.Mistral = _FakeMistral
341
  monkeypatch.setitem(sys.modules, "mistralai", fake)
342
 
343
- MistralProvider().generate_content(b"jpeg", "mon prompt", "mistral-large-latest")
344
 
345
  assert len(captured) == 1
346
  assert captured[0]["content"] == "mon prompt"
@@ -485,7 +484,7 @@ def test_generate_content_ocr_model_not_called_for_vision(monkeypatch):
485
  fake.Mistral = _FakeMistral
486
  monkeypatch.setitem(sys.modules, "mistralai", fake)
487
 
488
- MistralProvider().generate_content(b"jpeg", "prompt", "pixtral-large-latest")
489
  assert len(ocr_called) == 0
490
 
491
 
 
88
 
89
 
90
  # ---------------------------------------------------------------------------
91
+ # _model_supports_vision() — détection dynamique via l'API
92
  # ---------------------------------------------------------------------------
93
 
94
+ def test_vision_detection_without_model_obj_returns_false():
95
+ """Sans objet modèle (pas de capabilities), retourne False par sécurité."""
96
+ assert _model_supports_vision("pixtral-large-latest") is False
97
+ assert _model_supports_vision("mistral-small-latest") is False
 
 
 
 
 
 
 
 
 
 
98
  assert _model_supports_vision("codestral-latest") is False
99
 
100
 
101
+ def test_vision_detection_uses_capabilities_from_api():
102
+ """La source de vérité est capabilities.vision retourné par l'API Mistral."""
103
  m_vision = _FakeModel("some-model", vision=True)
104
  m_text = _FakeModel("some-model", vision=False)
105
  assert _model_supports_vision("some-model", m_vision) is True
106
  assert _model_supports_vision("some-model", m_text) is False
107
 
108
 
109
+ def test_vision_detection_capabilities_false_on_any_model():
110
+ """capabilities.vision=False pas de vision, quel que soit le nom."""
111
  m = _FakeModel("pixtral-test", vision=False)
112
  assert _model_supports_vision("pixtral-test", m) is False
113
 
114
 
115
+ def test_vision_detection_capabilities_true_on_any_model():
116
+ """capabilities.vision=True → vision activée, quel que soit le nom."""
117
+ m = _FakeModel("mistral-small-latest", vision=True)
118
+ assert _model_supports_vision("mistral-small-latest", m) is True
119
+
120
+
121
  # ---------------------------------------------------------------------------
122
  # is_configured()
123
  # ---------------------------------------------------------------------------
 
266
  # ---------------------------------------------------------------------------
267
 
268
  def test_generate_content_vision_model_returns_text(monkeypatch):
269
+ """Modèle vision : envoie l'image et retourne la réponse."""
270
  monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
271
  fake = _make_fake_mistralai()
272
  monkeypatch.setitem(sys.modules, "mistralai", fake)
273
 
274
  result = MistralProvider().generate_content(
275
+ b"fake-jpeg", "Analyse ce folio.", "pixtral-large-latest",
276
+ supports_vision=True,
277
  )
278
  assert result == "Voici le JSON de la page."
279
 
280
 
281
  def test_generate_content_text_model_returns_text(monkeypatch):
282
+ """Modèle texte (supports_vision=False) : envoie seulement le prompt."""
283
  monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
284
  fake = _make_fake_mistralai()
285
  monkeypatch.setitem(sys.modules, "mistralai", fake)
286
 
287
  result = MistralProvider().generate_content(
288
+ b"fake-jpeg", "Analyse ce folio.", "mistral-large-latest",
289
+ supports_vision=False,
290
  )
291
  assert result == "Voici le JSON de la page."
292
 
 
310
  fake.Mistral = _FakeMistral
311
  monkeypatch.setitem(sys.modules, "mistralai", fake)
312
 
313
+ MistralProvider().generate_content(b"jpeg", "prompt", "pixtral-large-latest", supports_vision=True)
314
 
315
  assert len(captured) == 1
316
  content = captured[0]["content"]
 
321
 
322
 
323
  def test_generate_content_text_sends_string_content(monkeypatch):
324
+ """Modèle texte (supports_vision=False) : le message content est une chaîne (pas d'image)."""
325
  monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
326
  captured: list[dict] = []
327
 
 
339
  fake.Mistral = _FakeMistral
340
  monkeypatch.setitem(sys.modules, "mistralai", fake)
341
 
342
+ MistralProvider().generate_content(b"jpeg", "mon prompt", "mistral-large-latest", supports_vision=False)
343
 
344
  assert len(captured) == 1
345
  assert captured[0]["content"] == "mon prompt"
 
484
  fake.Mistral = _FakeMistral
485
  monkeypatch.setitem(sys.modules, "mistralai", fake)
486
 
487
+ MistralProvider().generate_content(b"jpeg", "prompt", "pixtral-large-latest", supports_vision=True)
488
  assert len(ocr_called) == 0
489
 
490
 
frontend/src/lib/api.ts CHANGED
@@ -280,11 +280,13 @@ export const selectModel = (
280
  modelId: string,
281
  displayName: string,
282
  providerType: string,
 
283
  ): Promise<CorpusModelConfig> =>
284
  put(`/api/v1/corpora/${corpusId}/model`, {
285
  model_id: modelId,
286
  display_name: displayName,
287
  provider_type: providerType,
 
288
  })
289
 
290
  export const deleteCorpus = (id: string): Promise<void> =>
@@ -295,6 +297,7 @@ export interface CorpusModelConfig {
295
  selected_model_id: string
296
  selected_model_display_name: string
297
  provider_type: string
 
298
  updated_at: string
299
  }
300
 
 
280
  modelId: string,
281
  displayName: string,
282
  providerType: string,
283
+ supportsVision: boolean = true,
284
  ): Promise<CorpusModelConfig> =>
285
  put(`/api/v1/corpora/${corpusId}/model`, {
286
  model_id: modelId,
287
  display_name: displayName,
288
  provider_type: providerType,
289
+ supports_vision: supportsVision,
290
  })
291
 
292
  export const deleteCorpus = (id: string): Promise<void> =>
 
297
  selected_model_id: string
298
  selected_model_display_name: string
299
  provider_type: string
300
+ supports_vision: boolean
301
  updated_at: string
302
  }
303
 
frontend/src/pages/Admin.tsx CHANGED
@@ -190,7 +190,7 @@ function ModelPanel({ corpusId, onSaved }: ModelPanelProps) {
190
  setSavingModel(true)
191
  const model = models.find((m) => m.model_id === selectedModelId)
192
  try {
193
- await selectModel(corpusId, selectedModelId, model?.display_name ?? selectedModelId, selectedProvider)
194
  const updated = await getCorpusModel(corpusId)
195
  setCurrentModel(updated)
196
  setSaveSuccess(`Modele "${model?.display_name ?? selectedModelId}" associe.`)
 
190
  setSavingModel(true)
191
  const model = models.find((m) => m.model_id === selectedModelId)
192
  try {
193
+ await selectModel(corpusId, selectedModelId, model?.display_name ?? selectedModelId, selectedProvider, model?.supports_vision ?? true)
194
  const updated = await getCorpusModel(corpusId)
195
  setCurrentModel(updated)
196
  setSaveSuccess(`Modele "${model?.display_name ?? selectedModelId}" associe.`)