From 1a435a6d5dae8fbbae31d9f29a1f8ee9f86d2809 Mon Sep 17 00:00:00 2001 From: Elshayib <30467832+Elshayib@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:51:49 +0530 Subject: [PATCH] fix(model-switch): prevent custom-provider misattribution in model picker (#48305) When the current provider is a custom endpoint (custom or custom:*), the model switch pipeline must NOT auto-switch to a native provider/OpenRouter based on a static-catalog match. The user explicitly configured their own endpoint and the same model name may be served there; silently rewriting model.provider destroys their config. - detect_static_provider_for_model(): skip the static-catalog scan when the current provider is custom/custom:* - switch_model() Step e: extend is_custom to cover custom:* so the detect_provider_for_model() last-resort fallback cannot fire Salvaged from #48351 by Elshayib (authorship preserved). Fixes #48305 --- hermes_cli/model_switch.py | 6 ++++-- hermes_cli/models.py | 10 ++++++++++ tests/hermes_cli/test_models.py | 20 ++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) 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."""