nesquena/hermes-webui

Composer chip and sidebar show configured default model, not the actual model after gateway failover

オープン

#6,594 opened on 2026/07/29

 (1 件のコメント) (0 件のリアクション) (0 人の担当者)Python (2,386 件のフォーク)github user discovery
buggatewayhelp wantedsprint-candidateux

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:

  1. config.yaml sets model.default: custom:LongCat-2.0
  2. The user opens a new conversation. The composer chip shows "LongCat 2.0"
  3. The Hermes gateway fails to reach LongCat and falls back to gemini-2.5-flash
  4. The composer chip still reads "LongCat 2.0" even though gemini-2.5-flash processed 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

  1. session.model is a static label, not the live model. It's set once and never updated when the gateway actually routes to a different provider/model.

  2. The composer chip would work correctly if gateway_routing.used_model were 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 through hermes gateway).

  3. The sidebar session list ignores gateway routing entirely — it formats all models from S.session.model alone 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

コントリビューターガイド