fix(image_routing): check stripped custom:<name> provider key for vision override

When model.provider is set to custom:<name>, _supports_vision_override()
previously tried only the runtime provider key ('custom') and the raw
config value ('custom:my-proxy'). It did not try the stripped name
('my-proxy'), which is the actual key under providers: in config.yaml.

This caused native image routing to fall back to text mode even when the
user explicitly declared supports_vision: true on the named provider's
model entry.

Fixes #39963
This commit is contained in:
liuhao1024 2026-06-06 01:04:14 +08:00 committed by Teknium
parent 7485fe0605
commit 5e11628546
2 changed files with 40 additions and 3 deletions

View file

@ -185,7 +185,8 @@ def _supports_vision_override(
2. ``providers.<provider>.models.<model>.supports_vision``
(named custom providers ``provider`` may be the runtime-resolved
value ``"custom"`` and/or the user-declared name under
``model.provider``; both are tried)
``model.provider``; both are tried. For ``custom:<name>`` syntax,
the stripped ``<name>`` is also tried as a provider key.)
Returns None when no override is set, so the caller falls through to
models.dev. Returns False explicitly only when the user wrote a
@ -205,11 +206,16 @@ 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.
# both as candidate provider keys, plus the stripped suffix from
# "custom:<name>" (where <name> is the key under providers:).
config_provider = str(model_cfg.get("provider") or "").strip()
# Extract the stripped name from "custom:<name>" if present
stripped_suffix = ""
if config_provider.startswith("custom:"):
stripped_suffix = config_provider[len("custom:"):]
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))):
for p in dict.fromkeys(filter(None, (provider, config_provider, stripped_suffix))):
entry_raw = providers_cfg.get(p)
entry: Dict[str, Any] = entry_raw if isinstance(entry_raw, dict) else {}
models_raw = entry.get("models")

View file

@ -224,6 +224,37 @@ class TestSupportsVisionOverride:
cfg = {"model": "some-string", "providers": ["not-a-dict"]}
assert _supports_vision_override(cfg, "custom", "my-llava") is None
def test_custom_colon_name_stripped_suffix_lookup(self):
# model.provider: custom:my-proxy → should resolve stripped key "my-proxy"
cfg = {
"model": {"provider": "custom:my-proxy"},
"providers": {
"my-proxy": {"models": {"gpt-5.5": {"supports_vision": True}}},
},
}
assert _supports_vision_override(cfg, "custom", "gpt-5.5") is True
def test_custom_colon_name_stripped_suffix_false(self):
# Explicitly disabled vision on the stripped key.
cfg = {
"model": {"provider": "custom:my-proxy"},
"providers": {
"my-proxy": {"models": {"gpt-5.5": {"supports_vision": False}}},
},
}
assert _supports_vision_override(cfg, "custom", "gpt-5.5") is False
def test_custom_colon_name_no_stripped_key_falls_through(self):
# custom:my-proxy but providers only has "custom" — stripped key
# doesn't match, but "custom" does via runtime provider.
cfg = {
"model": {"provider": "custom:my-proxy"},
"providers": {
"custom": {"models": {"gpt-5.5": {"supports_vision": True}}},
},
}
assert _supports_vision_override(cfg, "custom", "gpt-5.5") is True
# ─── _lookup_supports_vision (override-aware) ────────────────────────────────