diff --git a/agent/image_routing.py b/agent/image_routing.py index 9941961cbc..34ffafc2ce 100644 --- a/agent/image_routing.py +++ b/agent/image_routing.py @@ -337,8 +337,12 @@ def _lookup_supports_vision( """Return True/False if vision capability can be resolved, None if unknown. Order: config ``supports_vision`` override → :data:`_VISION_PROBES` - (managed local runtime → models.dev catalog → Ollama probe → registered - ``ProviderProfile.supports_vision`` declaration). + (managed local runtime → models.dev catalog → Ollama probe). A plugin's + per-model ``ProviderProfile.model_capabilities`` reaches this through the + catalog probe. The provider-wide ``ProviderProfile.supports_vision`` flag is + deliberately NOT consulted: it declares that tool-result messages may carry + images (``tools.vision_tools``), not that every model on the provider can see + user attachments — relays such as ``router`` set it for mixed catalogs. """ # Named custom providers are canonicalized to ``provider="custom"``; the # original name lives in the context-local main runtime. Borrow it only on an diff --git a/tests/agent/test_models_dev_provider_capabilities.py b/tests/agent/test_models_dev_provider_capabilities.py index 25a33c3cb5..0fe65babd3 100644 --- a/tests/agent/test_models_dev_provider_capabilities.py +++ b/tests/agent/test_models_dev_provider_capabilities.py @@ -72,3 +72,21 @@ def test_partial_plugin_metadata_preserves_unknowns_and_catalog_fields(monkeypat assert unknown.context_window == 48000 assert unknown.supports_reasoning is None and unknown.supports_vision is None assert catalog == original + + +def test_provider_wide_vision_flag_gates_tool_results_not_attachments(monkeypatch): + """``ProviderProfile.supports_vision`` is a provider-wide tool-result wire capability; per-model attachment + routing comes only from ``model_capabilities``. Relays (``router``) set the flag over mixed catalogs, so + honouring it for attachments sent images to text-only models (reverted in 08feaa98b5b).""" + from agent.image_routing import decide_image_input_mode + from tools.vision_tools import _supports_media_in_tool_results + + _isolated_registry(monkeypatch) + monkeypatch.setattr(models_dev, "_load_model_overrides", lambda: {}) + providers.register_provider(ProviderProfile( + name="fixture-relay", supports_vision=True, + model_capabilities={"seeing": {"supports_vision": True}})) + + assert _supports_media_in_tool_results("fixture-relay", "undeclared") is True + assert decide_image_input_mode("fixture-relay", "undeclared", {}) == "text" + assert decide_image_input_mode("fixture-relay", "seeing", {}) == "native" diff --git a/tools/vision_tools.py b/tools/vision_tools.py index a0f05a5daf..63bb9d32e3 100644 --- a/tools/vision_tools.py +++ b/tools/vision_tools.py @@ -559,8 +559,8 @@ def _supports_media_in_tool_results(provider: str, model: str) -> bool: def _accepts_tool_result_images(provider: str, model: str, cfg: Optional[Dict[str, Any]]) -> bool: """One gate for both native lanes — the ``vision_analyze`` fast path and the ``computer_use`` capture route (#115248): the profile's ``supports_vision_tool_messages=False`` veto first, then either the provider's tool - results are known to carry media or the capability lookup (config override → catalog → probes → profile) - attests the model as vision-capable.""" + results are known to carry media or the per-model capability lookup (config override → catalog incl. the + profile's ``model_capabilities`` → local probes) attests the model as vision-capable.""" if _profile_rejects_tool_media(provider, model): return False if _supports_media_in_tool_results(provider, model):