NousResearch/hermes-agent

Active vs configured model display during fallback

Cluster 1615 · dashboard. Data as of

3 open items. Fix merged in #1660. Check it covers your case.

The short answer

1open issue
2open PRs
18items grouped
Known?
Yes. 18 related items grouped here, 5 issues already closed.
Fix event
A fix was merged to main in PR #1660. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PRs #1660, #133961. That is separate from the fix event.
Still open
1 issue and 2 PRs.

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.

Open issues1

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

Open PRs2

  • PR#41320fix(tui): display configured primary model in status bar, not fallback
  • PR#45980fix(gateway): show active fallback model in /model command

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #1660 fix(gateway): /model shows active fallback model instead of config default (#1615)

    merged to main

    files: gateway/run.py

    Show the diff
    • gateway/run.py
      @@ -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]] = {}
      @@ -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}`",
      @@ -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:
      @@ -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

    files: agent/auxiliary_client.py, agent/credential_pool.py, cli.py, hermes_cli/auth.py, run_agent.py

    Show the diff
    • agent/auxiliary_client.py
      @@ -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
       
      @@ -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
       
      @@ -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
       
       
      @@ -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
      @@ -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
      @@ -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,
      @@ -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
      @@ -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]
      @@ -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
      @@ -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
      @@ -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
  • #133961 fix(dashboard): badge follows the PTY runtime model, not config, during fallback

    merged to main

    files: web/src/components/ChatSidebar.test.tsx, web/src/components/ChatSidebar.tsx

7 PRs closed without merging

From the close comments: merged via another change 2 · closed without comment 1 · done elsewhere 1 · duplicate 1 · superseded by another PR 1 · other 1

  • #1616 fix(gateway): /model shows active fallback model instead of config default · merged via another change → #1660
  • #6399 fix(cli): update TUI status bar model name on provider fallback · merged via another change → #6754
  • #7430 fix(cli): sync CLI model/provider from agent after fallback activation · done elsewhere → 2f0a83d, #7385
  • #11514 fix(cli): show active fallback model in /model Current: line · closed without comment
  • #46091 fix(gateway): /model shows active fallback instead of stale config (#45970) · duplicate → #45980, #45970
Show 2 more
  • #54895 feat(webui): show runtime model and fallback indicator in sidebar badge · other → #6239, #5205, #54895, #57642
  • #67763 fix(webui): show active fallback model · superseded by another PR
Closed issues · 5
  • Closed#1615C:/Program Files/Git/model command shows config model instead of active fallback model
  • Closed#6398TUI status bar shows stale model name after fallback activation
  • Closed#7385[Bug]: CLI/status bar keeps showing configured primary model after fallback provider is active
  • Closed#41170[Bug]: Status bar displays fallback model name instead of active primary model
  • Closed#54509WebUI model badge does not reflect runtime model during fallback activation
How this cluster was grouped
  • Link evidence (partly structural links): 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 item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
dashboard
Merged PRs
#1660, #6754, #133961
Closed issues
5
Attempts
7 PRs closed without merging
Fix link checked
6 Oct 2026

For your agent

The same facts, typed.