fix(models): scope prefix routing to user-configured providers only

This commit is contained in:
liuhao1024
2026-08-16 03:35:52 +08:00
committed by Teknium
parent 4033f3fc5f
commit f82d2f1301
2 changed files with 47 additions and 24 deletions
+22 -17
View File
@@ -3640,14 +3640,21 @@ def _configured_provider_ids() -> set[str]:
def _resolve_provider_prefix(model_name: str) -> Optional[tuple[str, str]]:
"""Resolve an explicit ``vendor/model`` prefix to a known provider.
"""Resolve an explicit ``vendor/model`` prefix to a configured provider.
``nous/deepseek-v4-pro`` or ``ollama/qwen3.5:4b`` should route to the
named provider instead of falling back to the configured default (which
silently sends non-default models to the wrong endpoint, #87189). The
vendor counts as known when it is a built-in provider id/alias or a key
in the user's ``providers:`` config block. The returned model is the
suffix with the prefix stripped — the provider's API expects the bare id.
silently sends non-default models to the wrong endpoint, #87189).
Only vendors the user actually defined in their ``providers:`` config
block (by raw name or alias) are routed here. Built-in vendor prefixes
(``google/gemini-2.5-flash``, ``deepseek/deepseek-chat``) deliberately
stay on the existing catalog / OpenRouter-slug / default-provider path:
those slug forms are aggregator-native, and rerouting them to the vendor
provider would change established provider-switch behavior (see
``TestDenormalizeProviderSwitch`` in tests/hermes_cli/test_web_server.py).
The returned model is the suffix with the prefix stripped — the target
provider's API expects the bare id.
"""
if "/" not in model_name:
return None
@@ -3657,20 +3664,17 @@ def _resolve_provider_prefix(model_name: str) -> Optional[tuple[str, str]]:
if not vendor or not model:
return None
configured = _configured_provider_ids()
if not configured:
return None
# A provider block the user explicitly named (``ollama:``) wins over the
# built-in alias table, which may canonicalize the same name elsewhere
# (``ollama`` → ``custom``) and route to the wrong endpoint.
if vendor in configured:
return (vendor, model)
canonical = _PROVIDER_ALIASES.get(vendor, vendor)
known = (
canonical in _PROVIDER_LABELS
or canonical in _PROVIDER_MODELS
or canonical in configured
)
if not known:
return None
return (canonical, model)
if canonical in configured:
return (canonical, model)
return None
def detect_provider_for_model(
@@ -3709,11 +3713,12 @@ def detect_provider_for_model(
return ("openrouter", or_slug)
return None # already on openrouter with matching name
# --- Step 3: explicit ``vendor/model`` prefix naming a provider ---
# --- Step 3: explicit ``vendor/model`` prefix naming a configured provider ---
# Checked after the OpenRouter slug lookup so aggregator-native slugs
# (e.g. ``deepseek/deepseek-chat``) keep their existing routing; this
# step only catches names no catalog serves, which previously fell back
# to the configured default provider and 404'd (#87189).
# (e.g. ``deepseek/deepseek-chat``) keep their existing routing; only
# vendors the user defined in their ``providers:`` block route here,
# so catalog/default behavior for built-in vendor prefixes is unchanged
# (#87189).
prefix_match = _resolve_provider_prefix(name)
if prefix_match is not None:
return prefix_match
@@ -12,19 +12,37 @@ import hermes_cli.model_switch as model_switch
class TestVendorPrefixRouting:
"""detect_provider_for_model honors an explicit ``vendor/model`` prefix."""
"""detect_provider_for_model honors a ``vendor/model`` prefix for
providers the user actually configured in their ``providers:`` block."""
def test_builtin_provider_prefix_routes_to_provider(self, monkeypatch):
def test_configured_provider_prefix_routes_to_provider(self, monkeypatch):
monkeypatch.setattr(models, "_find_openrouter_slug", lambda _name: None)
monkeypatch.setattr(models, "_configured_provider_ids", lambda: {"nous"})
detected = models.detect_provider_for_model("nous/deepseek-v4-pro", "anthropic")
assert detected == ("nous", "deepseek-v4-pro")
def test_configured_provider_prefix_routes_to_provider(self, monkeypatch):
def test_local_provider_prefix_routes_to_provider(self, monkeypatch):
monkeypatch.setattr(models, "_find_openrouter_slug", lambda _name: None)
monkeypatch.setattr(models, "_configured_provider_ids", lambda: {"ollama"})
detected = models.detect_provider_for_model("ollama/qwen3.5:4b", "anthropic")
assert detected == ("ollama", "qwen3.5:4b")
def test_unconfigured_builtin_vendor_prefix_not_rerouted(self, monkeypatch):
"""Built-in vendor slugs keep catalog/default routing.
``google/gemini-2.5-flash`` is aggregator-native: the web config
field expects it to switch to OpenRouter, not to the Gemini provider
(``TestDenormalizeProviderSwitch`` in test_web_server.py). With no
user-configured provider for the vendor, prefix routing must stay
out of the way even when the models.dev catalog is unavailable.
"""
monkeypatch.setattr(models, "_find_openrouter_slug", lambda _name: None)
monkeypatch.setattr(models, "_configured_provider_ids", lambda: set())
detected = models.detect_provider_for_model(
"google/gemini-2.5-flash", "ollama-local"
)
assert detected is None
def test_configured_provider_wins_over_alias_canonicalization(self, monkeypatch):
"""A user-named ``ollama`` block must not be rewritten to ``custom``."""
monkeypatch.setattr(models, "_find_openrouter_slug", lambda _name: None)
@@ -33,15 +51,15 @@ class TestVendorPrefixRouting:
detected = models.detect_provider_for_model("ollama/qwen3.5:4b", "anthropic")
assert detected == ("ollama", "qwen3.5:4b")
def test_provider_alias_prefix_canonicalized(self, monkeypatch):
def test_provider_alias_prefix_canonicalized_when_configured(self, monkeypatch):
monkeypatch.setattr(models, "_find_openrouter_slug", lambda _name: None)
monkeypatch.setattr(models, "_configured_provider_ids", lambda: set())
monkeypatch.setattr(models, "_configured_provider_ids", lambda: {"zai"})
detected = models.detect_provider_for_model("glm/glm-4.7", "anthropic")
assert detected == ("zai", "glm-4.7")
def test_unknown_vendor_prefix_still_unmatched(self, monkeypatch):
monkeypatch.setattr(models, "_find_openrouter_slug", lambda _name: None)
monkeypatch.setattr(models, "_configured_provider_ids", lambda: set())
monkeypatch.setattr(models, "_configured_provider_ids", lambda: {"ollama"})
assert models.detect_provider_for_model("notaprovider/foo-model", "anthropic") is None
def test_openrouter_slug_still_wins_over_prefix_routing(self, monkeypatch):
@@ -49,7 +67,7 @@ class TestVendorPrefixRouting:
monkeypatch.setattr(
models, "_find_openrouter_slug", lambda _name: "deepseek/deepseek-chat"
)
monkeypatch.setattr(models, "_configured_provider_ids", lambda: set())
monkeypatch.setattr(models, "_configured_provider_ids", lambda: {"deepseek"})
detected = models.detect_provider_for_model("deepseek/deepseek-chat", "anthropic")
assert detected == ("openrouter", "deepseek/deepseek-chat")