From 6bd02ae1a67e6df147fc7433f26c13c300a7e84f Mon Sep 17 00:00:00 2001 From: happy5318 Date: Tue, 14 Jul 2026 00:31:14 +0800 Subject: [PATCH] feat(image_routing): accept vision alias for custom provider models Extend the existing candidate-name resolver in _supports_vision_override to accept 'vision' as an alias for 'supports_vision' on per-model config, for both the providers..models dict and the legacy list-style custom_providers form. Per review feedback on #31912: this extends the current resolver rather than replacing its candidate-name logic. Named custom providers resolve to the runtime value provider='custom' while the config keeps the user-declared name under model.provider; that lookup path is preserved. Adds regression tests covering model.provider=my-vllm with runtime provider='custom' for both config shapes. --- agent/image_routing.py | 12 +++- tests/agent/test_image_routing.py | 96 +++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/agent/image_routing.py b/agent/image_routing.py index b5475bb05ce5..a8337c9ac6ad 100644 --- a/agent/image_routing.py +++ b/agent/image_routing.py @@ -194,6 +194,10 @@ def _supports_vision_override( and/or the user-declared name under ``model.provider``; all are tried. For ``custom:`` syntax, the stripped ```` is also tried as a provider key.) + 2b. ``custom_providers`` (legacy list form) ``.models.`` + + Under (2) and (2b), the per-model capability key may be written as + either ``supports_vision`` or the shorter ``vision`` alias; both work. Returns None when no override is set, so the caller falls through to models.dev. Returns False explicitly only when the user wrote a @@ -234,7 +238,9 @@ def _supports_vision_override( models_cfg: Dict[str, Any] = models_raw if isinstance(models_raw, dict) else {} per_model_raw = models_cfg.get(model) per_model: Dict[str, Any] = per_model_raw if isinstance(per_model_raw, dict) else {} - coerced = _coerce_capability_bool(per_model.get("supports_vision")) + coerced = _coerce_capability_bool( + per_model.get("supports_vision", per_model.get("vision")) + ) if coerced is not None: return coerced @@ -258,7 +264,9 @@ def _supports_vision_override( models_cfg = models_raw if isinstance(models_raw, dict) else {} per_model_raw = models_cfg.get(model) per_model = per_model_raw if isinstance(per_model_raw, dict) else {} - coerced = _coerce_capability_bool(per_model.get("supports_vision")) + coerced = _coerce_capability_bool( + per_model.get("supports_vision", per_model.get("vision")) + ) if coerced is not None: return coerced diff --git a/tests/agent/test_image_routing.py b/tests/agent/test_image_routing.py index a591ea439512..4e9026753054 100644 --- a/tests/agent/test_image_routing.py +++ b/tests/agent/test_image_routing.py @@ -874,3 +874,99 @@ class TestFormatCompatibility: img_path.write_bytes(b'') url = _file_to_data_url(img_path) assert url is None + + +# ─── vision alias for custom providers ────────────────────────────────────── + + +class TestCustomProviderVisionAlias: + """`vision: true` should work as an alias for `supports_vision: true`. + + Covers both config shapes that host named custom providers: + * the ``providers..models`` dict, and + * the legacy list-style ``custom_providers`` entries. + + Regression for the review of PR #31912: named custom providers resolve + to the runtime value ``provider="custom"`` while the config keeps the + user-declared name under ``model.provider``. The existing candidate-name + resolver must be *extended* to accept the ``vision`` alias, not replaced. + """ + + def test_providers_dict_vision_alias_true(self): + cfg = { + "providers": { + "my-vllm": {"models": {"llava-v1.6": {"vision": True}}} + } + } + assert _supports_vision_override(cfg, "my-vllm", "llava-v1.6") is True + + def test_providers_dict_vision_alias_false(self): + cfg = { + "providers": { + "my-vllm": {"models": {"llama-3": {"vision": False}}} + } + } + assert _supports_vision_override(cfg, "my-vllm", "llama-3") is False + + def test_supports_vision_wins_over_vision_alias(self): + """When both keys are present, the canonical key takes priority.""" + cfg = { + "providers": { + "my-vllm": { + "models": { + "m": {"supports_vision": True, "vision": False} + } + } + } + } + assert _supports_vision_override(cfg, "my-vllm", "m") is True + + def test_named_custom_provider_bare_custom_runtime_vision_alias(self): + """Teknium's requested regression case. + + A named custom provider (``model.provider: my-vllm``) is rewritten to + the runtime value ``provider="custom"`` by + ``hermes_cli/runtime_provider.py``. The resolver must still match the + ``my-vllm`` entry via the ``model.provider`` candidate and honour the + ``vision`` alias. + """ + cfg = { + "model": {"provider": "my-vllm"}, + "providers": { + "my-vllm": {"models": {"llava-v1.6": {"vision": True}}} + }, + } + # Runtime provider is the bare normalized value "custom". + assert _supports_vision_override(cfg, "custom", "llava-v1.6") is True + assert decide_image_input_mode("custom", "llava-v1.6", cfg) == "native" + + def test_custom_providers_list_bare_custom_runtime_vision_alias(self): + """Same regression, but the provider lives in the legacy list form.""" + cfg = { + "model": {"provider": "my-vllm"}, + "custom_providers": [ + { + "name": "my-vllm", + "models": {"llava-v1.6": {"vision": True}}, + } + ], + } + assert _supports_vision_override(cfg, "custom", "llava-v1.6") is True + assert decide_image_input_mode("custom", "llava-v1.6", cfg) == "native" + + def test_custom_providers_list_vision_alias_false(self): + cfg = { + "model": {"provider": "my-vllm"}, + "custom_providers": [ + {"name": "my-vllm", "models": {"llama-3": {"vision": False}}} + ], + } + assert _supports_vision_override(cfg, "custom", "llama-3") is False + + def test_vision_alias_none_when_model_absent(self): + cfg = { + "custom_providers": [ + {"name": "my-vllm", "models": {"llava": {"vision": True}}} + ] + } + assert _supports_vision_override(cfg, "custom:my-vllm", "other") is None