Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plugins/model-providers/meta-ai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ def _base_url() -> str:
api_mode="codex_responses",
# Muse Spark is natively multimodal (image/video/pdf/audio in, text out).
supports_vision=True,
# ...but only on user turns: an image envelope inside a role:tool message
# 400s "messages[N].content did not match any supported type" (#101668).
supports_vision_tool_messages=False,
# Cheap contributor tier is a good default for auxiliary tasks
# (compaction, title generation, vision) when this is the main provider.
default_aux_model="muse-spark-1.2-contributor",
Expand Down
2 changes: 2 additions & 0 deletions tests/providers/test_meta_ai_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def test_profile_registered(self):
assert p.api_mode == "codex_responses"
assert "MODEL_API_KEY" in p.env_vars
assert p.supports_vision is True
# Images are accepted on user turns only; tool-result envelopes 400 (#101668).
assert p.supports_vision_tool_messages is False
assert p.default_aux_model == "muse-spark-1.2-contributor"
assert p.default_max_tokens == 16384
assert p.fallback_models == ("muse-spark-1.2",)
Expand Down
24 changes: 24 additions & 0 deletions tests/tools/test_vision_native_fast_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,30 @@ def test_empty_provider_no(self):
assert _supports_media_in_tool_results("", "anything") is False
assert _supports_media_in_tool_results(None, "anything") is False # type: ignore[arg-type]

def test_profile_tool_message_veto_overrides_supports_vision(self):
"""supports_vision_tool_messages=False is a hard veto even when the
profile declares supports_vision=True (xiaomi/MiMo 400s on list-type
tool-result content, #89981)."""
assert _supports_media_in_tool_results("xiaomi", "mimo-v2.5") is False

def test_profile_veto_applies_even_when_vision_capable_lookup_agrees(self):
"""A capability source marking the model vision-capable must not
re-open the native fast path for a provider that rejects it."""
from tools.vision_tools import _should_use_native_vision_fast_path
from agent.auxiliary_client import set_runtime_main, clear_runtime_main
from agent import image_routing

set_runtime_main("xiaomi", "mimo-v2.5")
try:
with patch.object(
image_routing, "decide_image_input_mode", return_value="native"
), patch.object(
image_routing, "_lookup_supports_vision", return_value=True
):
assert _should_use_native_vision_fast_path() is False
finally:
clear_runtime_main()


# ─── _build_native_vision_tool_result ────────────────────────────────────────

Expand Down
23 changes: 22 additions & 1 deletion tools/vision_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,22 @@ def _dims_ok(w: int, h: int) -> bool:
# ---------------------------------------------------------------------------


def _profile_rejects_tool_media(provider: str) -> bool:
"""Hard veto: the provider's ``ProviderProfile`` declares
``supports_vision_tool_messages=False`` — images are accepted in user
messages but list-type tool-result content is rejected with 400
(xiaomi/MiMo "text is not set"). ``supports_vision`` alone must not
override this, or the multimodal tool-result envelope 400s every turn
and the image never enters context (#89981).
"""
try:
from providers import get_provider_profile
profile = get_provider_profile(str(provider or "").strip().lower())
return profile is not None and profile.supports_vision_tool_messages is False
except Exception:
return False


def _supports_media_in_tool_results(provider: str, model: str) -> bool:
"""Whether the given provider+model combination accepts image content
inside a tool-result message.
Expand All @@ -1100,7 +1116,7 @@ def _supports_media_in_tool_results(provider: str, model: str) -> bool:
if not isinstance(provider, str):
return False
p = provider.strip().lower()
if not p:
if not p or _profile_rejects_tool_media(p):
return False

# Aggregators that route to multiple vendors — assume support since
Expand Down Expand Up @@ -1170,6 +1186,11 @@ def _should_use_native_vision_fast_path() -> bool:
cfg = load_config()
if decide_image_input_mode(provider, model, cfg) != "native":
return False
# The profile veto applies ahead of the capability lookup too: a
# model marked vision-capable by models.dev / custom_providers must
# not re-open the multimodal-envelope route the profile rejects.
if _profile_rejects_tool_media(provider):
return False
return (
_supports_media_in_tool_results(provider, model)
or _lookup_supports_vision(provider, model, cfg) is True
Expand Down
Loading