diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index fdb6e9f6e8a..d8ef62d74ce 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -1057,8 +1057,10 @@ def switch_model( # --- Step e: detect_provider_for_model() as last resort --- _base = current_base_url or "" - is_custom = current_provider in {"custom", "local"} or ( - "localhost" in _base or "127.0.0.1" in _base + is_custom = ( + current_provider in {"custom", "local"} + or current_provider.startswith("custom:") + or ("localhost" in _base or "127.0.0.1" in _base) ) if ( diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 098312ce2df..f98facea1cb 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -1882,6 +1882,14 @@ def detect_static_provider_for_model( return None # --- Step 1: check static provider catalogs for a direct match --- + # If the current provider is a custom endpoint (custom or custom:*), never + # auto-switch away from it based on a static catalog match — the user + # explicitly configured their own endpoint and the same model name may be + # served there (#48305). + _is_custom_current = ( + current_provider == "custom" + or current_provider.startswith("custom:") + ) for pid, models in _PROVIDER_MODELS.items(): if ( pid in current_keys @@ -1889,6 +1897,8 @@ def detect_static_provider_for_model( or pid in _BORROWED_MODEL_PROVIDERS ): continue + if _is_custom_current: + continue if any(name_lower == m.lower() for m in models): return (pid, name) diff --git a/tests/hermes_cli/test_models.py b/tests/hermes_cli/test_models.py index 72179fb04b2..2f087635662 100644 --- a/tests/hermes_cli/test_models.py +++ b/tests/hermes_cli/test_models.py @@ -301,6 +301,26 @@ class TestDetectProviderForModel: assert result is not None assert result[0] not in {"nous",} # nous has claude models but shouldn't be suggested + def test_custom_provider_not_overridden_by_static_catalog(self): + """When current provider is custom:*, a static-catalog match must NOT + override it — otherwise a model served by the user's own endpoint gets + misattributed to a native provider, rewriting model.provider (#48305). + + `gpt-5.4` is in the static openai catalog; with current=custom:foo, + detection must return None instead of switching to openai. + """ + assert detect_provider_for_model("gpt-5.4", "custom:foo") is None + + def test_bare_custom_provider_not_overridden_by_static_catalog(self): + """Same protection for the bare 'custom' provider.""" + assert detect_provider_for_model("gpt-5.4", "custom") is None + + def test_non_custom_provider_detection_unaffected(self): + """The custom-provider guard must NOT change detection for non-custom + current providers — a static-catalog model still routes normally.""" + result = detect_provider_for_model("gpt-5.4", "openrouter") + assert result is not None and result[0] == "openai" + class TestIsNousFreeTier: """Tests for is_nous_free_tier — account tier detection."""