fix(chat): show requested and actual reply models

Show requested and actual reply models in chat labels when fallback or provider routing changes the responding model.
This commit is contained in:
Mohammed Riaz
2026-06-06 14:30:16 +04:00
committed by GitHub
parent 2e37d72155
commit 6ccd4500d7
8 changed files with 285 additions and 38 deletions
+62 -25
View File
@@ -53,7 +53,27 @@ import { createStreamRenderer } from './streamingRenderer.js';
// shortModel and modelColor are now in chatRenderer.js
var _shortModel = chatRenderer.shortModel;
var _modelRouteLabel = chatRenderer.modelRouteLabel;
var _sameModelName = chatRenderer.sameModelName;
var _applyModelColor = chatRenderer.applyModelColor;
function _setRoleModelLabel(roleEl, requestedModel, actualModel, opts) {
if (!roleEl) return;
opts = opts || {};
const tsSpan = roleEl.querySelector('.role-timestamp');
const req = requestedModel || actualModel || '';
const actual = actualModel || requestedModel || '';
let label = _modelRouteLabel(req, actual);
if (opts.suffix) label += ' (' + opts.suffix + ')';
if (opts.characterName) label = opts.characterName;
roleEl.textContent = label + ' ';
_applyModelColor(roleEl, actual || req);
if (req && actual && !_sameModelName(req, actual)) {
roleEl.title = req + ' -> ' + actual + (opts.reason ? ': ' + opts.reason : '');
} else if (!opts.reason) {
roleEl.removeAttribute('title');
}
if (tsSpan) roleEl.appendChild(tsSpan);
}
// Per-session research tracking (supports concurrent research across sessions)
const _researchingStreamIds = new Set();
let _researchTimerEl = null, _researchTimerInterval = null;
@@ -556,7 +576,6 @@ import { createStreamRenderer } from './streamingRenderer.js';
let _thinkOpen = false;
let holder = null;
let finalMeta = null;
let finalModelName = null;
let spinner = null;
let timedOut = false;
let processingProbeTimer = null;
@@ -892,11 +911,13 @@ import { createStreamRenderer } from './streamingRenderer.js';
loadingText = 'Processing request...';
}
var roleLabel = _shortModel(modelName);
var roleLabel = _modelRouteLabel(modelName, modelName);
var _charNameInit = presetsModule.getCharacterName ? presetsModule.getCharacterName() : '';
if (_charNameInit) roleLabel = _charNameInit;
const roleTs = new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
holder.innerHTML = `<div class="role">${uiModule.esc(roleLabel)} <span class="role-timestamp">${roleTs}</span></div><div class="body"></div>`;
holder._requestedModel = modelName;
holder._actualModel = modelName;
_applyModelColor(holder.querySelector('.role'), modelName);
holder.style.position = 'relative';
@@ -1807,21 +1828,16 @@ import { createStreamRenderer } from './streamingRenderer.js';
if (!_isBg && holder) {
const roleEl = holder.querySelector('.role');
if (roleEl) {
const tsSpan = roleEl.querySelector('.role-timestamp');
var _modelLabel = _shortModel(json.model);
if (json.suffix) {
_modelLabel += ' (' + json.suffix + ')';
holder._roleSuffix = json.suffix;
}
holder._requestedModel = json.requested_model || json.model || holder._requestedModel;
holder._actualModel = json.model || holder._actualModel || holder._requestedModel;
if (json.suffix) holder._roleSuffix = json.suffix;
// Prepend character name if sent by server or set locally
var _charName = json.character_name || (presetsModule.getCharacterName ? presetsModule.getCharacterName() : '');
if (_charName) {
_modelLabel = _charName;
holder._characterName = _charName;
}
roleEl.textContent = _modelLabel + ' ';
_applyModelColor(roleEl, json.model);
if (tsSpan) roleEl.appendChild(tsSpan);
if (_charName) holder._characterName = _charName;
_setRoleModelLabel(roleEl, holder._requestedModel, holder._actualModel, {
suffix: holder._roleSuffix,
characterName: holder._characterName,
});
}
}
} else if (json.type === 'fallback') {
@@ -1841,6 +1857,14 @@ import { createStreamRenderer } from './streamingRenderer.js';
(json.reason ? ': ' + json.reason : '') + ' — answered by ' + (json.answered_by || '');
_applyModelColor(_rEl, json.answered_by);
if (_tsS) _rEl.appendChild(_tsS);
holder._requestedModel = json.selected_model || holder._requestedModel || modelName;
const _hasResolvedActual = holder._actualModel && !_sameModelName(holder._actualModel, holder._requestedModel);
holder._actualModel = _hasResolvedActual ? holder._actualModel : (json.answered_by || holder._actualModel || holder._requestedModel);
_setRoleModelLabel(_rEl, holder._requestedModel, holder._actualModel, {
suffix: holder._roleSuffix,
characterName: holder._characterName,
reason: json.reason,
});
}
}
}
@@ -1882,6 +1906,15 @@ import { createStreamRenderer } from './streamingRenderer.js';
_chatBox.appendChild(note);
try { note.scrollIntoView({ block: 'end', behavior: 'smooth' }); } catch (_) { uiModule.scrollHistory && uiModule.scrollHistory(); }
}
} else if (json.type === 'model_actual') {
if (!_isBg && holder) {
holder._requestedModel = json.requested_model || holder._requestedModel || modelName;
holder._actualModel = json.model || holder._actualModel || holder._requestedModel;
_setRoleModelLabel(holder.querySelector('.role'), holder._requestedModel, holder._actualModel, {
suffix: holder._roleSuffix,
characterName: holder._characterName,
});
}
} else if (json.type === 'attachments') {
if (_isBg) continue;
// Update user bubble — replace file chips with image previews
@@ -1959,6 +1992,10 @@ import { createStreamRenderer } from './streamingRenderer.js';
}
} else if (json.type === 'metrics') {
metrics = json.data;
if (!_isBg && holder && metrics) {
holder._requestedModel = metrics.requested_model || holder._requestedModel || modelName;
holder._actualModel = metrics.model || holder._actualModel || holder._requestedModel;
}
if (_isBg) {
var bgM = _backgroundStreams.get(streamSessionId);
if (bgM) bgM.metrics = json.data;
@@ -2441,8 +2478,10 @@ import { createStreamRenderer } from './streamingRenderer.js';
const newRole = document.createElement('div');
newRole.className = 'role';
const metaS = sessionModule.getSessions().find(s => s.id === streamSessionId);
newRole.textContent = _shortModel(metaS?.model) || '';
_applyModelColor(newRole, metaS?.model);
const _roundRequested = holder?._requestedModel || metaS?.model;
const _roundActual = holder?._actualModel || _roundRequested;
newRole.textContent = _modelRouteLabel(_roundRequested, _roundActual) || '';
_applyModelColor(newRole, _roundActual);
newWrap.appendChild(newRole);
const newBody = document.createElement('div');
newBody.className = 'body';
@@ -2548,18 +2587,16 @@ import { createStreamRenderer } from './streamingRenderer.js';
const _isBgFinal = (sessionModule.getCurrentSessionId() !== streamSessionId) || _backgroundStreams.has(streamSessionId);
if (!_isBgFinal) {
finalMeta = sessionModule.getSessions().find(s => s.id === sessionModule.getCurrentSessionId());
finalModelName = _shortModel(metrics?.model || finalMeta?.model);
// Preserve suffix (e.g. "Research") if set by model_info event
if (holder._roleSuffix) finalModelName += ' (' + holder._roleSuffix + ')';
const _finalActualModel = metrics?.model || holder._actualModel || finalMeta?.model;
const _finalRequestedModel = metrics?.requested_model || holder._requestedModel || finalMeta?.model || _finalActualModel;
// Prepend character name if set
var _charNameFinal = presetsModule.getCharacterName ? presetsModule.getCharacterName() : '';
if (_charNameFinal) finalModelName = _charNameFinal;
const roleEl = holder.querySelector('.role');
if (roleEl) {
const tsSpan = roleEl.querySelector('.role-timestamp');
roleEl.textContent = finalModelName + ' ';
_applyModelColor(roleEl, metrics?.model || finalMeta?.model);
if (tsSpan) roleEl.appendChild(tsSpan);
_setRoleModelLabel(roleEl, _finalRequestedModel, _finalActualModel, {
suffix: holder._roleSuffix,
characterName: _charNameFinal || holder._characterName,
});
}
holder.dataset.raw = accumulated;
+53 -5
View File
@@ -537,6 +537,39 @@ export function shortModel(name) {
return short;
}
function modelValue(name) {
if (name == null) return '';
return String(name).trim();
}
export function sameModelName(left, right) {
const a = modelValue(left);
const b = modelValue(right);
if (!a || !b) return false;
return a.toLowerCase() === b.toLowerCase()
|| shortModel(a).toLowerCase() === shortModel(b).toLowerCase();
}
export function modelRouteLabel(requestedModel, actualModel) {
const requested = modelValue(requestedModel);
const actual = modelValue(actualModel) || requested;
if (!requested || sameModelName(requested, actual)) return shortModel(actual || requested);
return shortModel(requested) + ' -> ' + shortModel(actual);
}
export function replyModelPair(modelName, metadata) {
const meta = metadata || {};
const actualFromMeta = modelValue(meta.model || meta.actual_model);
const requestedFromMeta = modelValue(meta.requested_model || meta.selected_model);
if (actualFromMeta || requestedFromMeta) {
const actual = actualFromMeta || requestedFromMeta || modelValue(modelName);
const requested = requestedFromMeta || actual;
return { requestedModel: requested, actualModel: actual };
}
const fallback = modelValue(modelName);
return { requestedModel: fallback, actualModel: fallback };
}
/**
* Generate a consistent HSL color for a model name.
* Returns an hsl() string. The hue is derived from a string hash,
@@ -577,7 +610,11 @@ export function applyModelColor(roleEl, modelName) {
}
// Replace generic dot with provider logo if available
const logo = providerLogo(modelName);
if (logo && !roleEl.querySelector('.role-provider-logo')) {
const existingLogo = roleEl.querySelector('.role-provider-logo');
if (!logo) {
if (existingLogo) existingLogo.remove();
roleEl.classList.remove('has-logo');
} else if (!existingLogo) {
const span = document.createElement('span');
span.className = 'role-provider-logo';
span.innerHTML = logo;
@@ -1933,8 +1970,12 @@ export function addMessage(role, content, modelName, metadata) {
wrap.className = 'msg msg-ai' + (r > 0 ? ' msg-continuation' : '');
const roleEl = document.createElement('div');
roleEl.className = 'role';
const contModel = modelName || metadata?.model;
roleEl.textContent = shortModel(contModel);
const pair = replyModelPair(modelName, metadata);
const contModel = pair.actualModel || pair.requestedModel;
roleEl.textContent = modelRouteLabel(pair.requestedModel, contModel);
if (pair.requestedModel && contModel && !sameModelName(pair.requestedModel, contModel)) {
roleEl.title = pair.requestedModel + ' -> ' + contModel;
}
applyModelColor(roleEl, contModel);
if (r === 0) roleEl.appendChild(roleTimestamp(metadata?.timestamp));
wrap.appendChild(roleEl);
@@ -2057,8 +2098,9 @@ export function addMessage(role, content, modelName, metadata) {
r.className = 'role';
const isSlash = metadata?.source === 'slash';
const isCompacted = metadata?.compacted;
const resolvedModel = modelName || metadata?.model;
var _roleText = role === 'user' ? 'You' : (isSlash || isCompacted) ? 'Odysseus' : shortModel(resolvedModel);
const replyModels = replyModelPair(modelName, metadata);
const resolvedModel = replyModels.actualModel || replyModels.requestedModel;
var _roleText = role === 'user' ? 'You' : (isSlash || isCompacted) ? 'Odysseus' : modelRouteLabel(replyModels.requestedModel, resolvedModel);
if (role === 'assistant' && (metadata?.research || metadata?.research_clarification)) {
_roleText += ' (Research)';
}
@@ -2069,6 +2111,9 @@ export function addMessage(role, content, modelName, metadata) {
}
r.textContent = _roleText;
if (role !== 'user') {
if (!isSlash && !isCompacted && replyModels.requestedModel && resolvedModel && !sameModelName(replyModels.requestedModel, resolvedModel)) {
r.title = replyModels.requestedModel + ' -> ' + resolvedModel;
}
if (!isSlash && !isCompacted) applyModelColor(r, resolvedModel);
r.appendChild(roleTimestamp(metadata?.timestamp));
}
@@ -2335,6 +2380,9 @@ export function addMessage(role, content, modelName, metadata) {
const chatRenderer = {
shortModel,
sameModelName,
modelRouteLabel,
replyModelPair,
modelColor,
applyModelColor,
getModelCost,