{"repo":"NousResearch/hermes-agent","public_id":"1615","display_label":"Active vs configured model display during fallback","summary":"A recurring set of bugs and fixes across CLI, TUI, WebUI, and gateway components where the displayed model name does not correctly reflect whether the active runtime model or the configured primary model should be shown during fallback activation. Members disagree on which value is correct to display, suggesting ongoing design tension alongside the technical fixes.","subsystem":"dashboard","counts":{"members":18,"open_issues":1,"open_issues_distinct":1,"open_prs":2,"closed_issues":5,"merged_prs":3,"attempt_prs":9},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":1660},"resolution":{"state":"partially_resolved","by":"pr-1660,pr-133961"},"freshness":{"state":"verified","last_verified_at":"2026-10-06T15:44:40+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.09254901960784313},"fix_edges":[{"issue_number":1615,"pr_number":1660,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":5796,"pr_number":6754,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":6075,"pr_number":6754,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":6398,"pr_number":6754,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":54509,"pr_number":133961,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":1615,"title":"C:/Program Files/Git/model command shows config model instead of active fallback model","url":null,"state":"closed"},{"kind":"issue","number":6398,"title":"TUI status bar shows stale model name after fallback activation","url":null,"state":"closed"},{"kind":"issue","number":7385,"title":"[Bug]: CLI/status bar keeps showing configured primary model after fallback provider is active","url":null,"state":"closed"},{"kind":"issue","number":41170,"title":"[Bug]: Status bar displays fallback model name instead of active primary model","url":null,"state":"closed"},{"kind":"issue","number":45970,"title":"[Bug] Regression: /model shows config model instead of active fallback model (fix from #1615 lost in v0.15.0 refactor)","url":null,"state":"open"},{"kind":"issue","number":54509,"title":"WebUI model badge does not reflect runtime model during fallback activation","url":null,"state":"closed"},{"kind":"pr","number":1616,"title":"fix(gateway): /model shows active fallback model instead of config default","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#1660","number":1660}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":1660,"title":"fix(gateway): /model shows active fallback model instead of config default (#1615)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py"],"fix_hunks":[{"file":"gateway/run.py","hunks":["@@ -342,7 +342,13 @@ def __init__(self, config: Optional[GatewayConfig] = None):\n         # Key: session_key, Value: AIAgent instance\n         self._running_agents: Dict[str, Any] = {}\n         self._pending_messages: Dict[str, str] = {}  # Queued messages during interrupt\n-        \n+\n+        # Track active fallback model/provider when primary is rate-limited.\n+        # Set after an agent run where fallback was activated; cleared when\n+        # the primary model succeeds again or the user switches via /model.\n+        self._effective_model: Optional[str] = None\n+        self._effective_provider: Optional[str] = None\n+\n         # Track pending exec approvals per session\n         # Key: session_key, Value: {\"command\": str, \"pattern_key\": str, ...}\n         self._pending_approvals: Dict[str, Dict[str, Any]] = {}","@@ -2204,6 +2210,21 @@ async def _handle_model_command(self, event: MessageEvent) -> str:\n             current_provider = \"custom\"\n \n         if not args:\n+            # If a fallback model is active, show it instead of config\n+            if self._effective_model:\n+                eff_provider = self._effective_provider or 'unknown'\n+                eff_label = _PROVIDER_LABELS.get(eff_provider, eff_provider)\n+                cfg_label = _PROVIDER_LABELS.get(current_provider, current_provider)\n+                lines = [\n+                    f\"🤖 **Active model:** `{self._effective_model}` (fallback)\",\n+                    f\"**Provider:** {eff_label}\",\n+                    f\"**Primary model** (`{current}` via {cfg_label}) is rate-limited.\",\n+                    \"\",\n+                ]\n+                lines.append(\"To change: `/model model-name`\")\n+                lines.append(\"Switch provider: `/model provider:model-name`\")\n+                return \"\\n\".join(lines)\n+\n             provider_label = _PROVIDER_LABELS.get(current_provider, current_provider)\n             lines = [\n                 f\"🤖 **Current model:** `{current}`\",","@@ -2303,6 +2324,9 @@ async def _handle_model_command(self, event: MessageEvent) -> str:\n             persist_note = \"saved to config\"\n         else:\n             persist_note = \"this session only — will revert on restart\"\n+        # Clear fallback state since user explicitly chose a model\n+        self._effective_model = None\n+        self._effective_provider = None\n         return f\"🤖 Model changed to `{new_model}` ({persist_note}){provider_note}{warning}\\n_(takes effect on next message)_\"\n \n     async def _handle_provider_command(self, event: MessageEvent) -> str:","@@ -4531,7 +4555,21 @@ async def monitor_for_interrupt():\n             # Run in thread pool to not block\n             loop = asyncio.get_event_loop()\n             response = await loop.run_in_executor(None, run_sync)\n-            \n+\n+            # Track fallback model state: if the agent switched to a\n+            # fallback model during this run, persist it so /model shows\n+            # the actually-active model instead of the config default.\n+            _agent = agent_holder[0]\n+            if _agent is not None and hasattr(_agent, 'model'):\n+                _cfg_model = _resolve_gateway_model()\n+                if _agent.model != _cfg_model:\n+                    self._effective_model = _agent.model\n+                    self._effective_provider = getattr(_agent, 'provider', None)\n+                else:\n+                    # Primary model worked — clear any stale fallback state\n+                    self._effective_model = None\n+                    self._effective_provider = None\n+\n             # Check if we were interrupted and have a pending message\n             result = result_holder[0]\n             adapter = self.adapters.get(source.platform)\n"]}]},{"kind":"pr","number":6399,"title":"fix(cli): update TUI status bar model name on provider fallback","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#6754","number":6754}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":6754,"title":"fix: fallback header preservation, kimi credential pool, TUI stale model display","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/auxiliary_client.py","agent/credential_pool.py","cli.py","hermes_cli/auth.py","run_agent.py"],"fix_hunks":[{"file":"agent/auxiliary_client.py","hunks":["@@ -702,7 +702,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:\n             logger.debug(\"Auxiliary text client: %s (%s) via pool\", pconfig.name, model)\n             extra = {}\n             if \"api.kimi.com\" in base_url.lower():\n-                extra[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.0\"}\n+                extra[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.3\"}\n             elif \"api.githubcopilot.com\" in base_url.lower():\n                 from hermes_cli.models import copilot_default_headers\n ","@@ -721,7 +721,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:\n         logger.debug(\"Auxiliary text client: %s (%s)\", pconfig.name, model)\n         extra = {}\n         if \"api.kimi.com\" in base_url.lower():\n-            extra[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.0\"}\n+            extra[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.3\"}\n         elif \"api.githubcopilot.com\" in base_url.lower():\n             from hermes_cli.models import copilot_default_headers\n ","@@ -1195,7 +1195,7 @@ def _to_async_client(sync_client, model: str):\n \n         async_kwargs[\"default_headers\"] = copilot_default_headers()\n     elif \"api.kimi.com\" in base_lower:\n-        async_kwargs[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.0\"}\n+        async_kwargs[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.3\"}\n     return AsyncOpenAI(**async_kwargs), model\n \n ","@@ -1315,7 +1315,13 @@ def resolve_provider_client(\n                 )\n                 return None, None\n             final_model = model or _read_main_model() or \"gpt-4o-mini\"\n-            client = OpenAI(api_key=[redacted:secret-assignment] base_url=custom_base)\n+            extra = {}\n+            if \"api.kimi.com\" in custom_base.lower():\n+                extra[\"default_headers\"] = {\"User-Agent\": \"KimiCLI/1.3\"}\n+            elif \"api.githubcopilot.com\" in custom_base.lower():\n+                from hermes_cli.models import copilot_default_headers\n+                extra[\"default_headers\"] = copilot_default_headers()\n+            client = OpenAI(api_key=[redacted:secret-assignment] base_url=custom_base, **extra)\n             return (_to_async_client(client, final_model) if async_mode\n                     else (client, final_model))\n         # Try custom first, then codex, then API-key providers","@@ -1394,7 +1400,7 @@ def resolve_provider_client(\n         # Provider-specific headers\n         headers = {}\n         if \"api.kimi.com\" in base_url.lower():\n-            headers[\"User-Agent\"] = \"KimiCLI/1.0\"\n+            headers[\"User-Agent\"] = \"KimiCLI/1.3\"\n         elif \"api.githubcopilot.com\" in base_url.lower():\n             from hermes_cli.models import copilot_default_headers\n "]},{"file":"agent/credential_pool.py","hunks":["@@ -18,12 +18,14 @@\n from hermes_cli.auth import (\n     CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS,\n     DEFAULT_AGENT_KEY_MIN_TTL_SECONDS,\n+    KIMI_CODE_BASE_URL,\n     PROVIDER_REGISTRY,\n     _codex_access_token_is_expiring,\n     _decode_jwt_claims,\n     _import_codex_cli_tokens,\n     _load_auth_store,\n     _load_provider_state,\n+    _resolve_kimi_base_url,\n     _resolve_zai_base_url,\n     read_credential_pool,\n     write_credential_pool,","@@ -1084,7 +1086,9 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool\n         active_sources.add(source)\n         auth_type = AUTH_TYPE_OAUTH if provider == \"anthropic\" and not token.startswith(\"sk-ant-api\") else AUTH_TYPE_API_KEY\n         base_url = env_url or pconfig.inference_base_url\n-        if provider == \"zai\":\n+        if provider == \"kimi-coding\":\n+            base_url = _resolve_kimi_base_url(token, pconfig.inference_base_url, env_url)\n+        elif provider == \"zai\":\n             base_url = _resolve_zai_base_url(token, pconfig.inference_base_url, env_url)\n         changed |= _upsert_entry(\n             entries,"]},{"file":"cli.py","hunks":["@@ -1603,7 +1603,12 @@ def _build_context_bar(self, percent_used: Optional[int], width: int = 10) -> st\n         return f\"[{('█' * filled) + ('░' * max(0, width - filled))}]\"\n \n     def _get_status_bar_snapshot(self) -> Dict[str, Any]:\n-        model_name = self.model or \"unknown\"\n+        # Prefer the agent's model name — it updates on fallback.\n+        # self.model reflects the originally configured model and never\n+        # changes mid-session, so the TUI would show a stale name after\n+        # _try_activate_fallback() switches provider/model.\n+        agent = getattr(self, \"agent\", None)\n+        model_name = (getattr(agent, \"model\", None) or self.model or \"unknown\")\n         model_short = model_name.split(\"/\")[-1] if \"/\" in model_name else model_name\n         if model_short.endswith(\".gguf\"):\n             model_short = model_short[:-5]","@@ -1629,7 +1634,6 @@ def _get_status_bar_snapshot(self) -> Dict[str, Any]:\n             \"compressions\": 0,\n         }\n \n-        agent = getattr(self, \"agent\", None)\n         if not agent:\n             return snapshot\n "]},{"file":"hermes_cli/auth.py","hunks":["@@ -250,7 +250,7 @@ class ProviderConfig:\n # Kimi Code Endpoint Detection\n # =============================================================================\n \n-# Kimi Code (platform.kimi.ai) issues keys prefixed \"sk-kimi-\" that only work\n+# Kimi Code (kimi.com/code) issues keys prefixed \"sk-kimi-\" that only work\n # on api.kimi.com/coding/v1.  Legacy keys from platform.moonshot.ai work on\n # api.moonshot.ai/v1 (the default).  Auto-detect when user hasn't set\n # KIMI_BASE_URL explicitly."]},{"file":"run_agent.py","hunks":["@@ -4968,9 +4968,21 @@ def _try_activate_fallback(self) -> bool:\n                 # Swap OpenAI client and config in-place\n                 self.api_key = [redacted:secret-assignment]\n                 self.client = fb_client\n+                # Preserve provider-specific headers that\n+                # resolve_provider_client() may have baked into\n+                # fb_client via the default_headers kwarg.  The OpenAI\n+                # SDK stores these in _custom_headers.  Without this,\n+                # subsequent request-client rebuilds (via\n+                # _create_request_openai_client) drop the headers,\n+                # causing 403s from providers like Kimi Coding that\n+                # require a User-Agent sentinel.\n+                fb_headers = getattr(fb_client, \"_custom_headers\", None)\n+                if not fb_headers:\n+                    fb_headers = getattr(fb_client, \"default_headers\", None)\n                 self._client_kwargs = {\n                     \"api_key\": fb_client.api_key,\n                     \"base_url\": fb_base_url,\n+                    **({\"default_headers\": dict(fb_headers)} if fb_headers else {}),\n                 }\n \n             # Re-evaluate prompt caching for the new provider/model\n"]}]},{"kind":"pr","number":7430,"title":"fix(cli): sync CLI model/provider from agent after fallback activation","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"2f0a83dd126d66dacc6ccbc88a98a964e443586b","number":0},{"kind":"pr","value":"#7385","number":7385}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":11514,"title":"fix(cli): show active fallback model in /model Current: line","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":41320,"title":"fix(tui): display configured primary model in status bar, not fallback","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":45980,"title":"fix(gateway): show active fallback model in /model command","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":46091,"title":"fix(gateway): /model shows active fallback instead of stale config (#45970)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#45980","number":45980},{"kind":"pr","value":"#45980","number":45980},{"kind":"pr","value":"#45970","number":45970}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":54895,"title":"feat(webui): show runtime model and fallback indicator in sidebar badge","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#6239","number":6239},{"kind":"pr","value":"#5205","number":5205},{"kind":"pr","value":"#6239","number":6239},{"kind":"pr","value":"#54895","number":54895},{"kind":"pr","value":"#6239","number":6239},{"kind":"pr","value":"#57642","number":57642},{"kind":"pr","value":"#6239","number":6239},{"kind":"pr","value":"#6239","number":6239}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":67763,"title":"fix(webui): show active fallback model","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":133961,"title":"fix(dashboard): badge follows the PTY runtime model, not config, during fallback","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["web/src/components/ChatSidebar.test.tsx","web/src/components/ChatSidebar.tsx"]}],"trust":{"default_tier":"verified","includes_suggested":false}}