Composer chip and sidebar show configured default model, not the actual model after gateway failover
#6,594 opened on 2026/07/29
Repository metrics
- Stars
- (17,368 個のスター)
- PR merge metrics
- (平均マージ 14h 31m) (30d で 314 merged PRs)
説明
Problem
The WebUI's model display (composer chip label + sidebar session list) persistently shows the configured default model from config.yaml even when the Hermes gateway has transparently routed the request to a different model due to failover, provider fallback, or model alias resolution.
Example scenario:
config.yamlsetsmodel.default: custom:LongCat-2.0- The user opens a new conversation. The composer chip shows "LongCat 2.0"
- The Hermes gateway fails to reach LongCat and falls back to
gemini-2.5-flash - The composer chip still reads "LongCat 2.0" even though
gemini-2.5-flashprocessed every turn
This is misleading — the user thinks they're testing a particular model's behavior, when in reality they have no feedback that a different model is serving their session.
Code Analysis
Where session.model is set (never updated after fallback)
api/routes.py ~20220-20222 — _persist_chat_state()
s.model = model # set once at session start from dropdown value or profile default
It is never re-assigned after the gateway resolves routing. The gateway routing metadata (gateway_routing.used_model, gateway_routing.model_changed) is stored on the session object but session.model itself is not mutated to reflect the actually-used model.
How the frontend reads the model
Composer chip — static/ui.js:3425 (syncModelChip):
const text = opt ? opt.textContent : getModelLabel(sel.value || '');
const gatewayRouting = _latestGatewayRoutingForSession(S.session);
const displayText = _formatGatewayModelLabel(sel.value || '', compactText, gatewayRouting) || compactText;
label.textContent = displayText;
_formatGatewayModelLabel (ui.js:6466) does check routing.used_model:
function _formatGatewayModelLabel(modelId, labelText, routing) {
if (!routing) return '';
const usedModel = String(routing.used_model || '').trim();
const base = usedModel
? _compactComposerModelChipLabel(usedModel, getModelLabel(usedModel))
: _compactComposerModelChipLabel(modelId, labelText || getModelLabel(modelId));
...
}
So the composer chip already has the machinery to show the real model — but only when session.gateway_routing is populated with a used_model that differs from the requested model.
Sidebar session list — static/sessions.js:1263:
opt.textContent = typeof getModelLabel === 'function'
? getModelLabel(S.session.model)
: S.session.model;
This reads S.session.model directly, with no gateway-routing-aware label formatting.
The sidebar response field allowlist — api/routes.py:9360-9423:
"gateway_routing" IS in the _SIDEBAR_SESSION_RESPONSE_FIELDS set, so the data is available — but the session list renderer doesn't use it.
The Gap
-
session.modelis a static label, not the live model. It's set once and never updated when the gateway actually routes to a different provider/model. -
The composer chip would work correctly if
gateway_routing.used_modelwere reliably populated — but the routing object may be empty for WebUI-platform sessions that don't go through the Hermes gateway's routing layer (e.g., sessions served via the WebUI's own direct-provider integration rather than throughhermes gateway). -
The sidebar session list ignores gateway routing entirely — it formats all models from
S.session.modelalone with no fallback-awareness.
Desired Behavior
After a session has one or more completed assistant turns, the composer chip and sidebar should reflect the actual model that processed those turns, not the requested default. When the gateway routing data (used_model) differs from the requested model, the display should show the active model, optionally with an indicator like "Failover: LongCat → Gemini 2.5 Flash" or "Model switched" (already implemented in _gatewayFailoverText and _gatewayModelWarningText).
Proposed Approaches (do not implement)
Option A — Update session.model post-routing:
When the gateway returns a response with routing metadata, have the Python backend (api/routes.py) update session.model to the actual used_model if it differs from the requested model. This is the simplest fix — downstream consumers (sidebar, export, session recovery) all use session.model and would automatically get the correct value.
Option B — Extend sidebar formatting with gateway routing:
In static/sessions.js, apply the same gateway-routing-aware formatting that the composer chip already uses (_formatGatewayModelLabel). This requires gateway_routing to be reliably present on session objects, which it may not be for all session types.
Option C — Both: Fix the backend propagation (Option A) as the source-of-truth fix, and harden the sidebar rendering (Option B) as a display-level safety net.
Code References
| File | Lines | Role |
|---|---|---|
api/routes.py |
9360-9423 | _SIDEBAR_SESSION_RESPONSE_FIELDS — sidebar field allowlist |
api/routes.py |
6506-6565 | _read_profile_model_config() — reads default model from config.yaml |
api/routes.py |
7210-7227 | _resolve_compatible_session_model_state() — resolves effective model |
api/routes.py |
~20220-20222 | s.model = model — set once at session creation, never updated after routing |
static/ui.js |
3425-3450 | syncModelChip() — composer chip render |
static/ui.js |
6466-6497 | _formatGatewayModelLabel(), _gatewayFailoverText(), _gatewayModelWarningText() — gateway routing label formatting (already implemented) |
static/sessions.js |
411-418 | _formatSessionModelWithGateway() — per-session label helper |
static/sessions.js |
~1263 | Session list item — reads S.session.model directly without routing context |