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
- 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.
Next step: read #1660, then check whether your report is one of the 1 still open.
Read #1660Pick up a Lab taskA 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
Fixes on record
-
#1660 fix(gateway): /model shows active fallback model instead of config default (#1615)
files:
gateway/run.pyShow 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
files:
agent/auxiliary_client.py,agent/credential_pool.py,cli.py,hermes_cli/auth.py,run_agent.pyShow 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
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
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.00means 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