# NousResearch/hermes-agent · Active vs configured model display during fallback

## Open now

- 1 issue, 2 PRs open on GitHub.
- #45970: \[Bug\] Regression: /model shows config model instead of active fallback model (fix from #1615 lost in v0.15.0 refactor)
- PR #41320: fix(tui): display configured primary model in status bar, not fallback
- PR #45980: fix(gateway): show active fallback model in /model command

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.

## TL;DR

- 18 member(s): 1 open issue(s), 5 closed issue(s), 2 open PR(s), 3 merged PR(s), 7 PR(s) closed without merging.
- A fix was merged to main in PR #1660. Verify it addresses your case.
- **Recommended:** read PR #1660 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-1660,pr-133961`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.09`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #45970: \[Bug\] Regression: /model shows config model instead of active fallback model (fix from #1615 lost in v0.15.0 refactor)

## Closed / duplicate issues

- #1615: C:/Program Files/Git/model command shows config model instead of active fallback model
- #6398: TUI status bar shows stale model name after fallback activation
- #7385: \[Bug\]: CLI/status bar keeps showing configured primary model after fallback provider is active
- #41170: \[Bug\]: Status bar displays fallback model name instead of active primary model
- #54509: WebUI model badge does not reflect runtime model during fallback activation

## Prior & competing PRs

- **#1660: fix(gateway): /model shows active fallback model instead of config default (#1615)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/run.py`
  - fix hunks:
  - `gateway/run.py`

    ```diff
    @@ -342,7 +342,13 @@ def __init__(self, config: Optional[GatewayConfig] = None):
             # Key: session_key, Value: AIAgent instance
             self._running_agents: Dict[str, Any] = {}
             self._pending_messages: Dict[str, str] = {}  # Queued messages during interrupt
    -        
    +
    +        # Track active fallback model/provider when primary is rate-limited.
    +        # Set after an agent run where fallback was activated; cleared when
    +        # the primary model succeeds again or the user switches via /model.
    +        self._effective_model: Optional[str] = None
    +        self._effective_provider: Optional[str] = None
    +
             # Track pending exec approvals per session
             # Key: session_key, Value: {"command": str, "pattern_key": str, ...}
             self._pending_approvals: Dict[str, Dict[str, Any]] = {}
    ```

    ```diff
    @@ -2204,6 +2210,21 @@ async def _handle_model_command(self, event: MessageEvent) -> str:
                 current_provider = "custom"
     
             if not args:
    +            # If a fallback model is active, show it instead of config
    +            if self._effective_model:
    +                eff_provider = self._effective_provider or 'unknown'
    +                eff_label = _PROVIDER_LABELS.get(eff_provider, eff_provider)
    +                cfg_label = _PROVIDER_LABELS.get(current_provider, current_provider)
    +                lines = [
    +                    f"🤖 **Active model:** `{self._effective_model}` (fallback)",
    +                    f"**Provider:** {eff_label}",
    +                    f"**Primary model** (`{current}` via {cfg_label}) is rate-limited.",
    +                    "",
    +                ]
    +                lines.append("To change: `/model model-name`")
    +                lines.append("Switch provider: `/model provider:model-name`")
    +                return "\n".join(lines)
    +
                 provider_label = _PROVIDER_LABELS.get(current_provider, current_provider)
                 lines = [
                     f"🤖 **Current model:** `{current}`",
    ```

    ```diff
    @@ -2303,6 +2324,9 @@ async def _handle_model_command(self, event: MessageEvent) -> str:
                 persist_note = "saved to config"
             else:
                 persist_note = "this session only — will revert on restart"
    +        # Clear fallback state since user explicitly chose a model
    +        self._effective_model = None
    +        self._effective_provider = None
             return f"🤖 Model changed to `{new_model}` ({persist_note}){provider_note}{warning}\n_(takes effect on next message)_"
     
         async def _handle_provider_command(self, event: MessageEvent) -> str:
    ```

    ```diff
    @@ -4531,7 +4555,21 @@ async def monitor_for_interrupt():
                 # Run in thread pool to not block
                 loop = asyncio.get_event_loop()
                 response = await loop.run_in_executor(None, run_sync)
    -            
    +
    +            # Track fallback model state: if the agent switched to a
    +            # fallback model during this run, persist it so /model shows
    +            # the actually-active model instead of the config default.
    +            _agent = agent_holder[0]
    +            if _agent is not None and hasattr(_agent, 'model'):
    +                _cfg_model = _resolve_gateway_model()
    +                if _agent.model != _cfg_model:
    +                    self._effective_model = _agent.model
    +                    self._effective_provider = getattr(_agent, 'provider', None)
    +                else:
    +                    # Primary model worked — clear any stale fallback state
    +                    self._effective_model = None
    +                    self._effective_provider = None
    +
                 # Check if we were interrupted and have a pending message
                 result = result_holder[0]
                 adapter = self.adapters.get(source.platform)
    ```
- **#6754: fix: fallback header preservation, kimi credential pool, TUI stale model display**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/auxiliary_client.py`, `agent/credential_pool.py`, `cli.py`, `hermes_cli/auth.py`, `run_agent.py`
  - fix hunks:
  - `agent/auxiliary_client.py`

    ```diff
    @@ -702,7 +702,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
                 logger.debug("Auxiliary text client: %s (%s) via pool", pconfig.name, model)
                 extra = {}
                 if "api.kimi.com" in base_url.lower():
    -                extra["default_headers"] = {"User-Agent": "KimiCLI/1.0"}
    +                extra["default_headers"] = {"User-Agent": "KimiCLI/1.3"}
                 elif "api.githubcopilot.com" in base_url.lower():
                     from hermes_cli.models import copilot_default_headers
     
    ```

    ```diff
    @@ -721,7 +721,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]:
             logger.debug("Auxiliary text client: %s (%s)", pconfig.name, model)
             extra = {}
             if "api.kimi.com" in base_url.lower():
    -            extra["default_headers"] = {"User-Agent": "KimiCLI/1.0"}
    +            extra["default_headers"] = {"User-Agent": "KimiCLI/1.3"}
             elif "api.githubcopilot.com" in base_url.lower():
                 from hermes_cli.models import copilot_default_headers
     
    ```

    ```diff
    @@ -1195,7 +1195,7 @@ def _to_async_client(sync_client, model: str):
     
             async_kwargs["default_headers"] = copilot_default_headers()
         elif "api.kimi.com" in base_lower:
    -        async_kwargs["default_headers"] = {"User-Agent": "KimiCLI/1.0"}
    +        async_kwargs["default_headers"] = {"User-Agent": "KimiCLI/1.3"}
         return AsyncOpenAI(**async_kwargs), model
     
     
    ```

    ```diff
    @@ -1315,7 +1315,13 @@ def resolve_provider_client(
                     )
                     return None, None
                 final_model = model or _read_main_model() or "gpt-4o-mini"
    -            client = OpenAI(api_key=[redacted:secret-assignment] base_url=custom_base)
    +            extra = {}
    +            if "api.kimi.com" in custom_base.lower():
    +                extra["default_headers"] = {"User-Agent": "KimiCLI/1.3"}
    +            elif "api.githubcopilot.com" in custom_base.lower():
    +                from hermes_cli.models import copilot_default_headers
    +                extra["default_headers"] = copilot_default_headers()
    +            client = OpenAI(api_key=[redacted:secret-assignment] base_url=custom_base, **extra)
                 return (_to_async_client(client, final_model) if async_mode
                         else (client, final_model))
             # Try custom first, then codex, then API-key providers
    ```

    ```diff
    @@ -1394,7 +1400,7 @@ def resolve_provider_client(
             # Provider-specific headers
             headers = {}
             if "api.kimi.com" in base_url.lower():
    -            headers["User-Agent"] = "KimiCLI/1.0"
    +            headers["User-Agent"] = "KimiCLI/1.3"
             elif "api.githubcopilot.com" in base_url.lower():
                 from hermes_cli.models import copilot_default_headers
     
    ```
  - `agent/credential_pool.py`

    ```diff
    @@ -18,12 +18,14 @@
     from hermes_cli.auth import (
         CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS,
         DEFAULT_AGENT_KEY_MIN_TTL_SECONDS,
    +    KIMI_CODE_BASE_URL,
         PROVIDER_REGISTRY,
         _codex_access_token_is_expiring,
         _decode_jwt_claims,
         _import_codex_cli_tokens,
         _load_auth_store,
         _load_provider_state,
    +    _resolve_kimi_base_url,
         _resolve_zai_base_url,
         read_credential_pool,
         write_credential_pool,
    ```

    ```diff
    @@ -1084,7 +1086,9 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool
             active_sources.add(source)
             auth_type = AUTH_TYPE_OAUTH if provider == "anthropic" and not token.startswith("sk-ant-api") else AUTH_TYPE_API_KEY
             base_url = env_url or pconfig.inference_base_url
    -        if provider == "zai":
    +        if provider == "kimi-coding":
    +            base_url = _resolve_kimi_base_url(token, pconfig.inference_base_url, env_url)
    +        elif provider == "zai":
                 base_url = _resolve_zai_base_url(token, pconfig.inference_base_url, env_url)
             changed |= _upsert_entry(
                 entries,
    ```
  - `cli.py`

    ```diff
    @@ -1603,7 +1603,12 @@ def _build_context_bar(self, percent_used: Optional[int], width: int = 10) -> st
             return f"[{('█' * filled) + ('░' * max(0, width - filled))}]"
     
         def _get_status_bar_snapshot(self) -> Dict[str, Any]:
    -        model_name = self.model or "unknown"
    +        # Prefer the agent's model name — it updates on fallback.
    +        # self.model reflects the originally configured model and never
    +        # changes mid-session, so the TUI would show a stale name after
    +        # _try_activate_fallback() switches provider/model.
    +        agent = getattr(self, "agent", None)
    +        model_name = (getattr(agent, "model", None) or self.model or "unknown")
             model_short = model_name.split("/")[-1] if "/" in model_name else model_name
             if model_short.endswith(".gguf"):
                 model_short = model_short[:-5]
    ```

    ```diff
    @@ -1629,7 +1634,6 @@ def _get_status_bar_snapshot(self) -> Dict[str, Any]:
                 "compressions": 0,
             }
     
    -        agent = getattr(self, "agent", None)
             if not agent:
                 return snapshot
     
    ```
  - `hermes_cli/auth.py`

    ```diff
    @@ -250,7 +250,7 @@ class ProviderConfig:
     # Kimi Code Endpoint Detection
     # =============================================================================
     
    -# Kimi Code (platform.kimi.ai) issues keys prefixed "sk-kimi-" that only work
    +# Kimi Code (kimi.com/code) issues keys prefixed "sk-kimi-" that only work
     # on api.kimi.com/coding/v1.  Legacy keys from platform.moonshot.ai work on
     # api.moonshot.ai/v1 (the default).  Auto-detect when user hasn't set
     # KIMI_BASE_URL explicitly.
    ```
  - `run_agent.py`

    ```diff
    @@ -4968,9 +4968,21 @@ def _try_activate_fallback(self) -> bool:
                     # Swap OpenAI client and config in-place
                     self.api_key = [redacted:secret-assignment]
                     self.client = fb_client
    +                # Preserve provider-specific headers that
    +                # resolve_provider_client() may have baked into
    +                # fb_client via the default_headers kwarg.  The OpenAI
    +                # SDK stores these in _custom_headers.  Without this,
    +                # subsequent request-client rebuilds (via
    +                # _create_request_openai_client) drop the headers,
    +                # causing 403s from providers like Kimi Coding that
    +                # require a User-Agent sentinel.
    +                fb_headers = getattr(fb_client, "_custom_headers", None)
    +                if not fb_headers:
    +                    fb_headers = getattr(fb_client, "default_headers", None)
                     self._client_kwargs = {
                         "api_key": fb_client.api_key,
                         "base_url": fb_base_url,
    +                    **({"default_headers": dict(fb_headers)} if fb_headers else {}),
                     }
     
                 # Re-evaluate prompt caching for the new provider/model
    ```
- **#7430: fix(cli): sync CLI model/provider from agent after fallback activation**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `2f0a83dd126d66dacc6ccbc88a98a964e443586b`, `#7385`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#54895: feat(webui): show runtime model and fallback indicator in sidebar badge**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#6239`, `#5205`, `#6239`, `#54895`, `#6239`, `#57642`, `#6239`, `#6239`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#133961: fix(dashboard): badge follows the PTY runtime model, not config, during fallback**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `web/src/components/ChatSidebar.test.tsx`, `web/src/components/ChatSidebar.tsx`
- **#1616: fix(gateway): /model shows active fallback model instead of config default**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#1660`
- **#6399: fix(cli): update TUI status bar model name on provider fallback**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#6754`
- **#11514: fix(cli): show active fallback model in /model Current: line**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#41320: fix(tui): display configured primary model in status bar, not fallback**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `agent/agent_init.py`, `tests/test_tui_gateway_server.py`, `tui_gateway/server.py`
- **#45980: fix(gateway): show active fallback model in /model command**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `gateway/slash_commands.py`, `tests/gateway/test_model_command_fallback_display.py`
- **#46091: fix(gateway): /model shows active fallback instead of stale config (#45970)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#45980`, `#45980`, `#45970`
- **#67763: fix(webui): show active fallback model**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`

## Fix status

- Merged to main in PR #1660 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-10-06`.
- Subsystem: `dashboard`
