{"repo":"NousResearch/hermes-agent","public_id":"16857","display_label":"Model switching routing and provider resolution bugs","summary":"These issues and fixes address problems where model-switching commands incorrectly route to native providers instead of configured or custom providers, leak environment/process state across sessions, and fail to honor launch-time or runtime provider overrides.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":16897,"merged_at":"2026-04-28T08:17:05+00:00"},"resolution":{"state":"resolved","by":"pr-16897"},"freshness":{"state":"stale","last_verified_at":"2026-04-28T08:17:05+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":16857,"pr_number":16897,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-28T08:17:05+00:00"}],"members":[{"kind":"issue","number":16857,"title":"TUI: /new after /model switch to custom provider model fails with 'no API key' when model name exists in native provider catalog","url":null,"state":"closed"},{"kind":"pr","number":16873,"title":"fix(gateway): honour HERMES_INFERENCE_PROVIDER in _resolve_startup_runtime (#16857)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#16897","number":16897},{"kind":"pr","value":"#16857","number":16857},{"kind":"pr","value":"#15755","number":15755}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":16897,"title":"fix(tui): `/model` writes HERMES_TUI_PROVIDER unconditionally (#16857)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/test_tui_gateway_server.py","tui_gateway/server.py"],"fix_hunks":[{"file":"tests/test_tui_gateway_server.py","hunks":["@@ -1171,6 +1171,61 @@ def switch_model(self, **_kwargs):\n     assert os.environ[\"HERMES_INFERENCE_PROVIDER\"] == \"anthropic\"\n \n \n+def test_config_set_model_syncs_tui_provider_unconditionally(monkeypatch):\n+    \"\"\"Regression for #16857: /model must set HERMES_TUI_PROVIDER even when\n+    it wasn't pre-set on launch, so a later /new (which re-runs\n+    _resolve_startup_runtime) honours the user's explicit provider choice\n+    instead of falling through to static-catalog detection and picking a\n+    coincidentally-matching native provider.\n+    \"\"\"\n+\n+    class _Agent:\n+        provider = \"openrouter\"\n+        model = \"old/model\"\n+        base_url = \"\"\n+        api_key = \"sk-or\"\n+\n+        def switch_model(self, **_kwargs):\n+            return None\n+\n+    result = types.SimpleNamespace(\n+        success=True,\n+        new_model=\"deepseek-v4-pro\",\n+        target_provider=\"custom:xuanji\",\n+        api_key=\"[redacted:secret-assignment]\",\n+        base_url=\"https://xuanji.example/v1\",\n+        api_mode=\"chat_completions\",\n+        warning_message=\"\",\n+    )\n+\n+    server._sessions[\"sid\"] = _session(agent=_Agent())\n+    monkeypatch.delenv(\"HERMES_TUI_PROVIDER\", raising=False)\n+    monkeypatch.delenv(\"HERMES_INFERENCE_PROVIDER\", raising=False)\n+    monkeypatch.setattr(\n+        \"hermes_cli.model_switch.switch_model\", lambda **_kwargs: result\n+    )\n+    monkeypatch.setattr(server, \"_restart_slash_worker\", lambda session: None)\n+    monkeypatch.setattr(server, \"_emit\", lambda *args, **kwargs: None)\n+\n+    server.handle_request(\n+        {\n+            \"id\": \"1\",\n+            \"method\": \"config.set\",\n+            \"params\": {\n+                \"session_id\": \"sid\",\n+                \"key\": \"model\",\n+                \"value\": \"deepseek-v4-pro --provider custom:xuanji\",\n+            },\n+        }\n+    )\n+\n+    # Both env vars must reflect the user's choice. HERMES_TUI_PROVIDER is\n+    # the canonical explicit-this-process carrier consumed by\n+    # _resolve_startup_runtime() on /new.\n+    assert os.environ[\"HERMES_TUI_PROVIDER\"] == \"custom:xuanji\"\n+    assert os.environ[\"HERMES_INFERENCE_PROVIDER\"] == \"custom:xuanji\"\n+\n+\n def test_config_set_model_syncs_tui_provider_env(monkeypatch):\n     class Agent:\n         model = \"gpt-5.3-codex\""]},{"file":"tui_gateway/server.py","hunks":["@@ -843,14 +843,19 @@ def _apply_model_switch(sid: str, session: dict, raw_input: str) -> dict:\n \n     os.environ[\"HERMES_MODEL\"] = result.new_model\n     os.environ[\"HERMES_INFERENCE_MODEL\"] = result.new_model\n-    # Keep the process-level provider env var in sync with the user's explicit\n-    # choice so any ambient re-resolution (credential pool refresh, compressor\n-    # rebuild, aux clients) resolves to the new provider instead of the\n-    # original one persisted in config or env.\n+    # Keep the process-level provider env vars in sync with the user's\n+    # explicit choice so any ambient re-resolution (credential pool refresh,\n+    # compressor rebuild, aux clients) and startup re-resolution on /new\n+    # both pick up the new provider instead of the original one persisted\n+    # in config or env.\n+    #\n+    # HERMES_TUI_PROVIDER is the canonical \"explicit-this-process\" carrier\n+    # consumed by _resolve_startup_runtime() — set it unconditionally on\n+    # /model so /new can't fall through to static-catalog detection and\n+    # pick a coincidentally-matching native provider (fixes #16857).\n     if result.target_provider:\n         os.environ[\"HERMES_INFERENCE_PROVIDER\"] = result.target_provider\n-        if os.environ.get(\"HERMES_TUI_PROVIDER\"):\n-            os.environ[\"HERMES_TUI_PROVIDER\"] = result.target_provider\n+        os.environ[\"HERMES_TUI_PROVIDER\"] = result.target_provider\n     if persist_global:\n         _persist_model_switch(result)\n     return {\"value\": result.new_model, \"warning\": result.warning_message or \"\"}\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}