fix(image-routing): drop the provider-wide supports_vision probe; mirror registry via public types in setup test
ProviderProfile.supports_vision is documented as "the API accepts image content inside tool-result messages" -- a provider-wide wire capability, not a per-model user-image verdict. Treating it as the latter flipped every model on the bundled `router` relay (and models.dev-unknown meta-ai/xiaomi models) from text to native image parts. Per-model vision now comes from ProviderProfile.model_capabilities (#116570) through the existing models.dev probe. The setup-catalog test mirrored the plugin through auth._register_plugin_provider, which #116553 renames; build the ProviderConfig from public types instead so the test passes on both trees.
This commit is contained in:
@@ -310,25 +310,12 @@ def _probe_ollama(provider: str, model: str, cfg: Optional[Dict[str, Any]]) -> O
|
||||
return query_ollama_supports_vision(model, base_url, api_key=api_key)
|
||||
|
||||
|
||||
def _probe_provider_profile(provider: str, model: str, cfg: Optional[Dict[str, Any]]) -> Optional[bool]:
|
||||
"""Registered ``ProviderProfile.supports_vision`` — the same declaration the tool-result media
|
||||
path (``tools.vision_tools._supports_media_in_tool_results``) already trusts, so a plugin is not
|
||||
vision-capable on one surface and text-only on another. Provider-wide and opt-in: only an
|
||||
explicit ``True`` is a verdict (the field defaults to False, which means "not declared").
|
||||
Runs after the per-model catalogs so a catalog that knows the exact model still wins."""
|
||||
from providers import get_provider_profile
|
||||
|
||||
profile = get_provider_profile(provider)
|
||||
return True if profile is not None and profile.supports_vision is True else None
|
||||
|
||||
|
||||
# Capability probes after the config override, in priority order; each returns
|
||||
# True/False or None (unknown → next probe). Exceptions are logged and treated as None.
|
||||
_VISION_PROBES: Tuple[Tuple[str, Callable[..., Optional[bool]]], ...] = (
|
||||
("managed-runtime caps lookup", _probe_managed_runtime),
|
||||
("caps lookup", _probe_models_dev),
|
||||
("ollama vision probe", _probe_ollama),
|
||||
("provider profile declaration", _probe_provider_profile),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -693,25 +693,3 @@ class TestCodexContextVariantVisionLookup:
|
||||
# Ineligible alias: looked up verbatim, no capability gained.
|
||||
assert image_routing._probe_models_dev("openai-codex", "gpt-5.5-900k", {}) is None
|
||||
assert seen[-1] == "gpt-5.5-900k"
|
||||
|
||||
|
||||
# ─── registered ProviderProfile.supports_vision ──────────────────────────────
|
||||
|
||||
|
||||
class TestProviderProfileDeclaration:
|
||||
|
||||
def test_profile_declared_vision_routes_native_only_when_declared(self, monkeypatch):
|
||||
"""The routing probe honours the same ``supports_vision`` the tool-result media path reads:
|
||||
a plugin that declares it goes native; an undeclared plugin stays text (#116408)."""
|
||||
import providers
|
||||
from providers.base import ProviderProfile
|
||||
|
||||
for name, declared in (("scout-vision", True), ("scout-novision", False)):
|
||||
monkeypatch.setitem(
|
||||
providers._REGISTRY, name,
|
||||
ProviderProfile(name=name, auth_type="api_key", base_url="https://x.invalid/v1",
|
||||
supports_vision=declared))
|
||||
with patch("agent.image_routing._probe_models_dev", return_value=None), \
|
||||
patch("agent.image_routing._probe_managed_runtime", return_value=None):
|
||||
assert decide_image_input_mode("scout-vision", "any-model", {}) == "native"
|
||||
assert decide_image_input_mode("scout-novision", "any-model", {}) == "text"
|
||||
|
||||
@@ -24,7 +24,13 @@ def test_setup_offers_registered_provider_catalog(monkeypatch, live):
|
||||
)
|
||||
monkeypatch.setitem(providers._REGISTRY, profile.name, profile)
|
||||
monkeypatch.setattr(auth, "PROVIDER_REGISTRY", dict(auth.PROVIDER_REGISTRY))
|
||||
auth._register_plugin_provider(profile)
|
||||
# Mirror into the auth registry the way plugin discovery does; built from public types so the
|
||||
# helper is independent of the auth module's private mirroring function.
|
||||
pconfig = auth.ProviderConfig(
|
||||
profile.name, profile.display_name or profile.name, profile.auth_type, inference_base_url=profile.base_url)
|
||||
if profile.auth_type == "api_key" and profile.env_vars:
|
||||
pconfig = auth._api_key_provider(profile.name, profile.display_name or profile.name, profile.base_url, tuple(profile.env_vars), "")
|
||||
auth.PROVIDER_REGISTRY[profile.name] = pconfig
|
||||
monkeypatch.setattr(flows, "_ensure_flow_api_key", lambda *_: (None, "synthetic-test-key", False))
|
||||
monkeypatch.setattr(flows, "_env_base_url", lambda *_: "")
|
||||
monkeypatch.setattr(flows, "_prompt_base_url_override", lambda value, *_args, **_kwargs: value)
|
||||
|
||||
@@ -107,7 +107,7 @@ Full definition in `providers/base.py`. The most useful ones:
|
||||
| `default_headers` | `dict[str, str]` | Sent on every request (e.g. Copilot's `Editor-Version`) |
|
||||
=======
|
||||
| `fallback_models` | `tuple[str, ...]` | Curated list shown when live catalog fetch fails — in the `/model` picker AND the first-time `hermes setup` / `hermes model` API-key flow, which resolve the catalog the same way (`fetch_models()` merged curated-first with `fallback_models`; `fallback_models` alone when the fetch returns `None` or raises) |
|
||||
| `supports_vision` | bool | Declares the endpoint accepts image input. Consumed by the tool-result media path and by the per-turn image-routing probe (`agent/image_routing.decide_image_input_mode`): an image attached to a model of a `supports_vision=True` plugin goes native instead of through `vision_analyze` text, unless a config override or a models.dev per-model entry says otherwise |
|
||||
| `supports_vision` | bool | Declares the provider's API accepts image content inside **tool-result** messages (a provider-wide wire capability). Per-model user-image routing comes from `model_capabilities` / models.dev, not from this flag |
|
||||
| `default_headers` | `dict[str, str]` | Sent on every request (e.g. Copilot's `Editor-Version`); also forwarded by the default `fetch_models()` catalog request |
|
||||
>>>>>>> cac616e8159 (fix(vision): image routing honours ProviderProfile.supports_vision)
|
||||
| `fixed_temperature` | Any | `None` = use caller's value; `OMIT_TEMPERATURE` sentinel = don't send temperature at all (Kimi) |
|
||||
|
||||
Reference in New Issue
Block a user