fix(chat/ui): record model name in history for consistency (#7845)

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto
2026-01-03 18:05:33 +01:00
committed by GitHub
parent 4cd95b8a9d
commit 33cc0b8e13
2 changed files with 86 additions and 21 deletions

View File

@@ -750,6 +750,7 @@ function stopRequest() {
if (!activeChat) return;
const request = activeRequests.get(activeChat.id);
const requestModel = request?.model || null; // Get model before deleting request
if (request) {
if (request.controller) {
request.controller.abort();
@@ -779,7 +780,8 @@ function stopRequest() {
`<span class='error'>Request cancelled by user</span>`,
null,
null,
activeChat.id
activeChat.id,
requestModel
);
}
@@ -1231,7 +1233,8 @@ async function promptGPT(systemPrompt, input) {
startTime: requestStartTime,
tokensReceived: 0,
interval: null,
maxTokensPerSecond: 0
maxTokensPerSecond: 0,
model: model // Store the model used for this request
});
// Update reactive tracking for UI indicators
@@ -1271,21 +1274,27 @@ async function promptGPT(systemPrompt, input) {
return;
} else {
// Timeout error (controller was aborted by timeout, not user)
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
chatStore.add(
"assistant",
`<span class='error'>Request timeout: MCP processing is taking longer than expected. Please try again.</span>`,
null,
null,
chatId
chatId,
requestModel
);
}
} else {
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
chatStore.add(
"assistant",
`<span class='error'>Network Error: ${error.message}</span>`,
null,
null,
chatId
chatId,
requestModel
);
}
toggleLoader(false, chatId);
@@ -1299,12 +1308,15 @@ async function promptGPT(systemPrompt, input) {
}
if (!response.ok) {
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
chatStore.add(
"assistant",
`<span class='error'>Error: POST ${endpoint} ${response.status}</span>`,
null,
null,
chatId
chatId,
requestModel
);
toggleLoader(false, chatId);
activeRequests.delete(chatId);
@@ -1324,12 +1336,15 @@ async function promptGPT(systemPrompt, input) {
.getReader();
if (!reader) {
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
chatStore.add(
"assistant",
`<span class='error'>Error: Failed to decode MCP API response</span>`,
null,
null,
chatId
chatId,
requestModel
);
toggleLoader(false, chatId);
activeRequests.delete(chatId);
@@ -1598,12 +1613,15 @@ async function promptGPT(systemPrompt, input) {
break;
case "error":
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
chatStore.add(
"assistant",
`<span class='error'>MCP Error: ${eventData.message}</span>`,
null,
null,
chatId
chatId,
requestModel
);
break;
}
@@ -1624,9 +1642,11 @@ async function promptGPT(systemPrompt, input) {
// Update or create assistant message with processed regular content
const currentChat = chatStore.getChat(chatId);
if (!currentChat) break; // Chat was deleted
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
if (lastAssistantMessageIndex === -1) {
if (processedRegular && processedRegular.trim()) {
chatStore.add("assistant", processedRegular, null, null, chatId);
chatStore.add("assistant", processedRegular, null, null, chatId, requestModel);
lastAssistantMessageIndex = targetHistory.length - 1;
}
} else {
@@ -1706,7 +1726,9 @@ async function promptGPT(systemPrompt, input) {
lastMessage.html = DOMPurify.sanitize(marked.parse(lastMessage.content));
}
} else if (processedRegular && processedRegular.trim()) {
chatStore.add("assistant", processedRegular, null, null, chatId);
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
chatStore.add("assistant", processedRegular, null, null, chatId, requestModel);
lastAssistantMessageIndex = targetHistory.length - 1;
}
}
@@ -1754,7 +1776,9 @@ async function promptGPT(systemPrompt, input) {
lastMessage.html = DOMPurify.sanitize(marked.parse(lastMessage.content));
}
} else {
chatStore.add("assistant", finalRegular, null, null, chatId);
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
chatStore.add("assistant", finalRegular, null, null, chatId, requestModel);
}
}
@@ -1812,12 +1836,15 @@ async function promptGPT(systemPrompt, input) {
.getReader();
if (!reader) {
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
chatStore.add(
"assistant",
`<span class='error'>Error: Failed to decode API response</span>`,
null,
null,
chatId
chatId,
requestModel
);
toggleLoader(false, chatId);
activeRequests.delete(chatId);
@@ -1848,9 +1875,11 @@ async function promptGPT(systemPrompt, input) {
const addToChat = (token) => {
const currentChat = chatStore.getChat(chatId);
if (!currentChat) return; // Chat was deleted
chatStore.add("assistant", token, null, null, chatId);
// Count tokens for rate calculation (per chat)
// Get model from request for this chat
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
chatStore.add("assistant", token, null, null, chatId, requestModel);
// Count tokens for rate calculation (per chat)
if (request) {
const tokenCount = Math.ceil(token.length / 4);
request.tokensReceived += tokenCount;
@@ -2008,12 +2037,15 @@ async function promptGPT(systemPrompt, input) {
if (error.name !== 'AbortError' || !currentAbortController) {
const currentChat = chatStore.getChat(chatId);
if (currentChat) {
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
chatStore.add(
"assistant",
`<span class='error'>Error: Failed to process stream</span>`,
null,
null,
chatId
chatId,
requestModel
);
}
}

View File

@@ -276,12 +276,31 @@ SOFTWARE.
}
},
add(role, content, image, audio, targetChatId = null) {
add(role, content, image, audio, targetChatId = null, model = null) {
// If targetChatId is provided, add to that chat, otherwise use active chat
// This allows streaming to continue to the correct chat even if user switches
const chat = targetChatId ? this.getChat(targetChatId) : this.activeChat();
if (!chat) return;
// Determine model for this message:
// - If model is explicitly provided, use it (for assistant messages with specific model)
// - For user messages, use the current chat's model
// - For other messages (thinking, tool_call, etc.), inherit from previous message or use chat model
let messageModel = model;
if (!messageModel) {
if (role === "user") {
// User messages always use the current chat's model
messageModel = chat.model || "";
} else if (role === "assistant") {
// Assistant messages use the chat's model (should be set when request is made)
messageModel = chat.model || "";
} else {
// For thinking, tool_call, etc., try to inherit from last assistant message, or use chat model
const lastAssistant = chat.history.slice().reverse().find(m => m.role === "assistant");
messageModel = lastAssistant?.model || chat.model || "";
}
}
const N = chat.history.length - 1;
// For thinking, reasoning, tool_call, and tool_result messages, always create a new message
if (role === "thinking" || role === "reasoning" || role === "tool_call" || role === "tool_result") {
@@ -311,7 +330,7 @@ SOFTWARE.
// Reasoning, tool_call, and tool_result are always collapsed by default
const isMCPMode = chat.mcpMode || false;
const shouldExpand = (role === "thinking" && !isMCPMode) || false;
chat.history.push({ role, content, html: c, image, audio, expanded: shouldExpand });
chat.history.push({ role, content, html: c, image, audio, expanded: shouldExpand, model: messageModel });
// Auto-name chat from first user message
if (role === "user" && chat.name === "New Chat" && content.trim()) {
@@ -332,6 +351,10 @@ SOFTWARE.
if (audio && audio.length > 0) {
chat.history[N].audio = [...(chat.history[N].audio || []), ...audio];
}
// Preserve model if merging (don't overwrite)
if (!chat.history[N].model && messageModel) {
chat.history[N].model = messageModel;
}
} else {
let c = "";
const lines = content.split("\n");
@@ -343,7 +366,8 @@ SOFTWARE.
content,
html: c,
image: image || [],
audio: audio || []
audio: audio || [],
model: messageModel
});
// Auto-name chat from first user message
@@ -1248,11 +1272,20 @@ SOFTWARE.
</template>
<template x-if="message.role != 'user' && message.role != 'thinking' && message.role != 'reasoning' && message.role != 'tool_call' && message.role != 'tool_result'">
<div class="flex items-center space-x-2">
{{ if $galleryConfig }}
{{ if $galleryConfig.Icon }}<img src="{{$galleryConfig.Icon}}" class="rounded-lg mt-2 max-w-8 max-h-8 border border-[var(--color-primary-border)]/20">{{end}}
{{ end }}
<!-- Model icon - from message history, fallback to active chat -->
<template x-if="message.model && window.__galleryConfigs && window.__galleryConfigs[message.model] && window.__galleryConfigs[message.model].Icon">
<img :src="window.__galleryConfigs[message.model].Icon" class="rounded-lg mt-2 max-w-8 max-h-8 border border-[var(--color-primary-border)]/20">
</template>
<!-- Fallback: use active chat model if message doesn't have one -->
<template x-if="!message.model && $store.chat.activeChat() && $store.chat.activeChat().model && window.__galleryConfigs && window.__galleryConfigs[$store.chat.activeChat().model] && window.__galleryConfigs[$store.chat.activeChat().model].Icon">
<img :src="window.__galleryConfigs[$store.chat.activeChat().model].Icon" class="rounded-lg mt-2 max-w-8 max-h-8 border border-[var(--color-primary-border)]/20">
</template>
<!-- Final fallback: initial model from server -->
<template x-if="!message.model && (!$store.chat.activeChat() || !$store.chat.activeChat().model) && window.__galleryConfigs && window.__galleryConfigs['{{$model}}'] && window.__galleryConfigs['{{$model}}'].Icon">
<img :src="window.__galleryConfigs['{{$model}}'].Icon" class="rounded-lg mt-2 max-w-8 max-h-8 border border-[var(--color-primary-border)]/20">
</template>
<div class="flex flex-col flex-1">
<span class="text-xs font-semibold text-[var(--color-text-secondary)] mb-1">{{if .Model}}{{.Model}}{{else}}Assistant{{end}}</span>
<span class="text-xs font-semibold text-[var(--color-text-secondary)] mb-1" x-text="message.model || $store.chat.activeChat()?.model || '{{if .Model}}{{.Model}}{{else}}Assistant{{end}}'"></span>
<div class="flex-1 text-[var(--color-text-primary)] flex items-center space-x-2 min-w-0">
<div class="p-3 rounded-lg bg-[var(--color-bg-secondary)] border border-[var(--color-accent-border)]/20 shadow-lg max-w-full overflow-x-auto overflow-wrap-anywhere" x-html="message.html"></div>
<button @click="copyToClipboard(message.html)" title="Copy to clipboard" class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors p-1 flex-shrink-0">