From b3ff5fc5b1409479de62ccf77ede0facf9d6c09f Mon Sep 17 00:00:00 2001 From: Gille <4317663+helix4u@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:48:58 -0600 Subject: [PATCH] fix(vision): resolve namespaced custom provider overrides --- agent/image_routing.py | 19 ++++++++++++------- tests/agent/test_image_routing.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/agent/image_routing.py b/agent/image_routing.py index 8cfa085527c4..2cbe8399837e 100644 --- a/agent/image_routing.py +++ b/agent/image_routing.py @@ -210,16 +210,21 @@ def _supports_vision_override( # get rewritten to provider="custom" at runtime # (hermes_cli/runtime_provider.py:_resolve_named_custom_runtime), so the # config still holds the user-declared name under model.provider. Try - # both as candidate provider keys, plus the stripped suffix from - # "custom:" (where is the key under providers:). + # both as candidate provider keys. Either identity may use the + # "custom:" form while providers: is keyed by bare . config_provider = str(model_cfg.get("provider") or "").strip() - # Extract the stripped name from "custom:" if present - stripped_suffix = "" - if config_provider.startswith("custom:"): - stripped_suffix = config_provider[len("custom:"):] + provider_candidates: List[str] = [] + for candidate in (provider, config_provider): + if not candidate: + continue + provider_candidates.append(candidate) + if candidate.startswith("custom:"): + stripped_candidate = candidate[len("custom:"):] + if stripped_candidate: + provider_candidates.append(stripped_candidate) providers_raw = cfg.get("providers") providers_cfg: Dict[str, Any] = providers_raw if isinstance(providers_raw, dict) else {} - for p in dict.fromkeys(filter(None, (provider, config_provider, stripped_suffix))): + for p in dict.fromkeys(provider_candidates): entry_raw = providers_cfg.get(p) entry: Dict[str, Any] = entry_raw if isinstance(entry_raw, dict) else {} models_raw = entry.get("models") diff --git a/tests/agent/test_image_routing.py b/tests/agent/test_image_routing.py index 671e6aaa18dd..a591ea439512 100644 --- a/tests/agent/test_image_routing.py +++ b/tests/agent/test_image_routing.py @@ -244,6 +244,14 @@ class TestSupportsVisionOverride: } assert _supports_vision_override(cfg, "custom", "gpt-5.5") is True + def test_custom_colon_runtime_name_stripped_suffix_lookup(self): + cfg = { + "providers": { + "my-proxy": {"models": {"gpt-5.5": {"supports_vision": True}}}, + }, + } + assert _supports_vision_override(cfg, "custom:my-proxy", "gpt-5.5") is True + def test_custom_colon_name_stripped_suffix_false(self): # Explicitly disabled vision on the stripped key. cfg = { @@ -325,6 +333,26 @@ class TestAutoModeRespectsOverride: with patch("agent.models_dev.get_model_capabilities", return_value=None): assert decide_image_input_mode("custom", "qwen3.6-35b", cfg) == "native" + def test_auto_native_for_namespaced_runtime_custom_provider(self): + cfg = { + "providers": { + "my-proxy": { + "models": { + "qwen3.8-max-preview": {"supports_vision": True}, + }, + }, + }, + } + with patch("agent.models_dev.get_model_capabilities", return_value=None): + assert ( + decide_image_input_mode( + "custom:my-proxy", + "qwen3.8-max-preview", + cfg, + ) + == "native" + ) + def test_auto_text_for_custom_with_supports_vision_false(self): cfg = {"model": {"supports_vision": False}} with patch("agent.models_dev.get_model_capabilities", return_value=None):