diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index be1a095418..0e6aa4a5ec 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -18,7 +18,7 @@ from hermes_cli.providers import ( from hermes_cli.model_normalize import normalize_model_for_provider from agent.models_dev import ( ModelCapabilities, ModelInfo, get_model_capabilities, get_model_info, list_provider_models) -from utils import base_url_hostname, base_url_origin +from utils import base_url_host_matches, base_url_hostname, base_url_origin # Re-exported: callers/tests patch hermes_cli.model_switch.. from hermes_cli.model_switch_providers import list_authenticated_providers @@ -1309,6 +1309,17 @@ def _creds_for_current_provider(st: _Switch) -> None: st.resolve_runtime(requested=st.current_provider) except Exception: pass + # Bare ``custom``/``local`` sessions whose base_url is session-only (not a trusted config + # ``model.base_url``) re-resolve to the OpenRouter DEFAULT — a host the user never picked + # (#74143). Keep the session endpoint + key then; a config-backed custom URL still wins so + # key/endpoint rotation is not pinned to a stale session. + if ( + st.current_provider in {"custom", "local"} and st.current_base_url + and (not st.base_url or base_url_host_matches(st.base_url, "openrouter.ai")) + and not base_url_host_matches(st.current_base_url, "openrouter.ai") + ): + st.base_url, st.api_key = st.current_base_url, st.current_api_key + st.api_mode = determine_api_mode(st.current_provider, st.base_url) def _resolve_switch_credentials(st: _Switch) -> Optional[ModelSwitchResult]: diff --git a/tests/hermes_cli/test_model_switch_custom_providers.py b/tests/hermes_cli/test_model_switch_custom_providers.py index a5e4768742..40b87b09a7 100644 --- a/tests/hermes_cli/test_model_switch_custom_providers.py +++ b/tests/hermes_cli/test_model_switch_custom_providers.py @@ -2220,3 +2220,23 @@ def test_legacy_sentinel_catalog_still_resolves_and_migrates(tmp_path, monkeypat assert list(saved["models"]) == _LOCAL_CATALOG assert "__discovered_model_catalog__" not in saved["models"] assert "__explicit_model_allowlist__" not in saved["models"] + + +def test_same_provider_switch_on_session_only_custom_endpoint_keeps_endpoint(monkeypatch): + """#74143: a same-provider ``/model`` on a bare ``custom`` session whose base_url is NOT the + trusted config ``model.base_url`` must stay on that endpoint with its key — re-resolving from + config fell through to the OpenRouter default and moved the next turn to a host the user never + picked.""" + for var in ("OPENROUTER_API_KEY", "OPENROUTER_BASE_URL", "CUSTOM_BASE_URL", "CUSTOM_API_KEY", "OPENAI_API_KEY"): + monkeypatch.delenv(var, raising=False) + monkeypatch.setattr("hermes_cli.model_switch.load_config", lambda: {"model": {"provider": "openrouter", "default": "x"}}, raising=False) + monkeypatch.setattr( + "hermes_cli.models.probe_api_models", + lambda api_key, base_url, **kw: {"models": ["m-a", "m-b"], "url": base_url + "/models", "base_url": base_url, + "suggested_base_url": None, "used_fallback": False}) + + result = switch_model("m-b", "custom", "m-a", "http://10.0.0.5:8000/v1", "session-secret") + + assert result.success + assert result.base_url == "http://10.0.0.5:8000/v1" + assert result.api_key == "session-secret"