diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 62df7d1f21b9..8995f18471f1 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -1346,7 +1346,12 @@ def _apply_main_model_assignment( if api_key.strip(): model_cfg["api_key"] = api_key.strip() model_cfg.pop("api", None) - elif model_cfg.get("api_key") and new_provider != prev_provider: + elif (model_cfg.get("api_key") or model_cfg.get("api")) and new_provider != prev_provider: + # A stale endpoint secret can live under the legacy ``api`` alias with + # no ``api_key`` (the resolver still reads ``model.api`` as a key), so + # the switch-clears-the-key path must trigger on either field — else the + # old endpoint's secret survives in config.yaml and contaminates a later + # custom resolution. clear_model_endpoint_credentials scrubs both. clear_model_endpoint_credentials(model_cfg, clear_api_mode=False) if new_provider != prev_provider: clear_model_endpoint_credentials(model_cfg, clear_api_key=False) diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index 1ac14feee3a1..24afb5ec9bd3 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -3989,6 +3989,18 @@ class TestWebServerEndpoints: assert "api_key" not in out assert "api_mode" not in out + # switching providers when the stale secret lives under the legacy + # ``api`` alias only (no api_key) → it must be cleared too. The resolver + # reads ``model.api`` as a key, so leaving it behind keeps a secret in + # config.yaml that contaminates the next custom resolution. + out = _apply_main_model_assignment( + {"provider": "custom", "api": "sk-legacy-stale", "base_url": "http://endpoint-a/v1"}, + "openrouter", + "m", + ) + assert "api" not in out + assert "api_key" not in out + def test_parse_model_ids_handles_openai_and_bare_shapes(self): """Model discovery must tolerate the common /v1/models shapes and never raise (so a slightly non-standard local endpoint still works)."""