From 28028cce55d977631f9d05ecaef8b2f5e3980e20 Mon Sep 17 00:00:00 2001 From: ly-wang19 Date: Sat, 20 Jun 2026 17:09:52 +0800 Subject: [PATCH] fix(web): clear stale `api`-alias credential on provider switch in main-model assignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit c253b0738 added clear_model_endpoint_credentials() to scrub an old endpoint's inline secret (api_key, the legacy `api` alias, api_mode) when the web UI switches the main model to a different provider. But _apply_main_model_assignment gates the key-scrub path on model_cfg["api_key"] being truthy, so when the stale secret lives only under the legacy `api` alias (no api_key), a provider switch never clears it — the secret survives in config.yaml. model.api is a live credential read path (_resolve_openrouter_runtime reads `for k in ("api_key", "api")`), so the old endpoint's key contaminates a later custom resolution — the exact harm clear_model_endpoint_credentials documents. The sibling persistence sites (the gateway model-picker paths and the aux-slot path) call the helper unconditionally on a non-custom switch and already scrub `api`; only this caller had the api_key-only gate. Widen the guard to fire on either field. The same-provider re-pick and explicit-new-key paths are unchanged. Adds the api-alias case to the assignment test (it fails without the fix). --- hermes_cli/web_server.py | 7 ++++++- tests/hermes_cli/test_web_server.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) 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)."""