diff --git a/core/http/endpoints/openai/chat.go b/core/http/endpoints/openai/chat.go
index 59f84e524..4ece68d5c 100644
--- a/core/http/endpoints/openai/chat.go
+++ b/core/http/endpoints/openai/chat.go
@@ -3,6 +3,7 @@ package openai
import (
"encoding/json"
"fmt"
+ "strings"
"time"
"github.com/google/uuid"
@@ -34,11 +35,54 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
Created: created,
Model: req.Model, // we have to return what the user sent here, due to OpenAI spec.
Choices: []schema.Choice{{Delta: &schema.Message{Role: "assistant"}, Index: 0, FinishReason: nil}},
- Object: "chat.completion.chunk",
}
responses <- initialMessage
+ // Track accumulated content for reasoning extraction
+ accumulatedContent := ""
+ lastEmittedReasoning := ""
+ lastEmittedCleanedContent := ""
+
_, _, err := ComputeChoices(req, s, config, cl, startupOptions, loader, func(s string, c *[]schema.Choice) {}, func(s string, tokenUsage backend.TokenUsage) bool {
+ accumulatedContent += s
+ // Extract reasoning from accumulated content
+ currentReasoning, cleanedContent := functions.ExtractReasoning(accumulatedContent)
+
+ // Calculate new reasoning delta (what we haven't emitted yet)
+ var reasoningDelta *string
+ if currentReasoning != lastEmittedReasoning {
+ // Extract only the new part
+ if len(currentReasoning) > len(lastEmittedReasoning) && strings.HasPrefix(currentReasoning, lastEmittedReasoning) {
+ newReasoning := currentReasoning[len(lastEmittedReasoning):]
+ reasoningDelta = &newReasoning
+ lastEmittedReasoning = currentReasoning
+ } else if currentReasoning != "" {
+ // If reasoning changed in a non-append way, emit the full current reasoning
+ reasoningDelta = ¤tReasoning
+ lastEmittedReasoning = currentReasoning
+ }
+ }
+
+ // Calculate content delta from cleaned content
+ var deltaContent string
+ if len(cleanedContent) > len(lastEmittedCleanedContent) && strings.HasPrefix(cleanedContent, lastEmittedCleanedContent) {
+ deltaContent = cleanedContent[len(lastEmittedCleanedContent):]
+ lastEmittedCleanedContent = cleanedContent
+ } else if cleanedContent != lastEmittedCleanedContent {
+ // If cleaned content changed but not in a simple append, extract delta from cleaned content
+ // This handles cases where thinking tags are removed mid-stream
+ if lastEmittedCleanedContent == "" {
+ deltaContent = cleanedContent
+ lastEmittedCleanedContent = cleanedContent
+ } else {
+ // Content changed in non-append way, use the new cleaned content
+ deltaContent = cleanedContent
+ lastEmittedCleanedContent = cleanedContent
+ }
+ }
+ // Only emit content if there's actual content (not just thinking tags)
+ // If deltaContent is empty, we still emit the response but with empty content
+
usage := schema.OpenAIUsage{
PromptTokens: tokenUsage.Prompt,
CompletionTokens: tokenUsage.Completion,
@@ -49,11 +93,20 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
usage.TimingPromptProcessing = tokenUsage.TimingPromptProcessing
}
+ delta := &schema.Message{}
+ // Only include content if there's actual content (not just thinking tags)
+ if deltaContent != "" {
+ delta.Content = &deltaContent
+ }
+ if reasoningDelta != nil && *reasoningDelta != "" {
+ delta.Reasoning = reasoningDelta
+ }
+
resp := schema.OpenAIResponse{
ID: id,
Created: created,
Model: req.Model, // we have to return what the user sent here, due to OpenAI spec.
- Choices: []schema.Choice{{Delta: &schema.Message{Content: &s}, Index: 0, FinishReason: nil}},
+ Choices: []schema.Choice{{Delta: delta, Index: 0, FinishReason: nil}},
Object: "chat.completion.chunk",
Usage: usage,
}
@@ -176,6 +229,10 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
if err != nil {
return err
}
+ // Extract reasoning before processing tool calls
+ reasoning, cleanedResult := functions.ExtractReasoning(result)
+ result = cleanedResult
+
textContentToReturn = functions.ParseTextContent(result, config.FunctionsConfig)
result = functions.CleanupLLMResult(result, config.FunctionsConfig)
functionResults := functions.ParseFunctionCall(result, config.FunctionsConfig)
@@ -208,11 +265,20 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
usage.TimingPromptProcessing = tokenUsage.TimingPromptProcessing
}
+ var deltaReasoning *string
+ if reasoning != "" {
+ deltaReasoning = &reasoning
+ }
+ delta := &schema.Message{Content: &result}
+ if deltaReasoning != nil {
+ delta.Reasoning = deltaReasoning
+ }
+
resp := schema.OpenAIResponse{
ID: id,
Created: created,
Model: req.Model, // we have to return what the user sent here, due to OpenAI spec.
- Choices: []schema.Choice{{Delta: &schema.Message{Content: &result}, Index: 0, FinishReason: nil}},
+ Choices: []schema.Choice{{Delta: delta, Index: 0, FinishReason: nil}},
Object: "chat.completion.chunk",
Usage: usage,
}
@@ -553,10 +619,18 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
default:
tokenCallback := func(s string, c *[]schema.Choice) {
+ // Extract reasoning from the response
+ reasoning, cleanedS := functions.ExtractReasoning(s)
+ s = cleanedS
+
if !shouldUseFn {
// no function is called, just reply and use stop as finish reason
stopReason := FinishReasonStop
- *c = append(*c, schema.Choice{FinishReason: &stopReason, Index: 0, Message: &schema.Message{Role: "assistant", Content: &s}})
+ message := &schema.Message{Role: "assistant", Content: &s}
+ if reasoning != "" {
+ message.Reasoning = &reasoning
+ }
+ *c = append(*c, schema.Choice{FinishReason: &stopReason, Index: 0, Message: message})
return
}
@@ -575,9 +649,13 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
}
stopReason := FinishReasonStop
+ message := &schema.Message{Role: "assistant", Content: &result}
+ if reasoning != "" {
+ message.Reasoning = &reasoning
+ }
*c = append(*c, schema.Choice{
FinishReason: &stopReason,
- Message: &schema.Message{Role: "assistant", Content: &result}})
+ Message: message})
default:
toolCallsReason := FinishReasonToolCalls
toolChoice := schema.Choice{
@@ -586,6 +664,9 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
Role: "assistant",
},
}
+ if reasoning != "" {
+ toolChoice.Message.Reasoning = &reasoning
+ }
for _, ss := range results {
name, args := ss.Name, ss.Arguments
@@ -606,16 +687,20 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
} else {
// otherwise we return more choices directly (deprecated)
functionCallReason := FinishReasonFunctionCall
+ message := &schema.Message{
+ Role: "assistant",
+ Content: &textContentToReturn,
+ FunctionCall: map[string]interface{}{
+ "name": name,
+ "arguments": args,
+ },
+ }
+ if reasoning != "" {
+ message.Reasoning = &reasoning
+ }
*c = append(*c, schema.Choice{
FinishReason: &functionCallReason,
- Message: &schema.Message{
- Role: "assistant",
- Content: &textContentToReturn,
- FunctionCall: map[string]interface{}{
- "name": name,
- "arguments": args,
- },
- },
+ Message: message,
})
}
}
diff --git a/core/http/static/chat.js b/core/http/static/chat.js
index 6c74a9c1c..6314f4f43 100644
--- a/core/http/static/chat.js
+++ b/core/http/static/chat.js
@@ -1368,6 +1368,7 @@ async function promptGPT(systemPrompt, input) {
let lastAssistantMessageIndex = -1;
let lastThinkingMessageIndex = -1;
let lastThinkingScrollTime = 0;
+ let hasReasoningFromAPI = false; // Track if we're receiving reasoning from API (skip tag-based detection)
const THINKING_SCROLL_THROTTLE = 200; // Throttle scrolling to every 200ms
try {
@@ -1401,19 +1402,24 @@ async function promptGPT(systemPrompt, input) {
// Handle different event types
switch (eventData.type) {
case "reasoning":
+ hasReasoningFromAPI = true; // Mark that we're receiving reasoning from API
if (eventData.content) {
- // Insert reasoning before assistant message if it exists
+ const currentChat = chatStore.getChat(chatId);
+ if (!currentChat) break; // Chat was deleted
+ const isMCPMode = currentChat.mcpMode || false;
+ const shouldExpand = !isMCPMode; // Expanded in non-MCP mode, collapsed in MCP mode
+ // Insert thinking before assistant message if it exists (always use "thinking" role)
if (lastAssistantMessageIndex >= 0 && targetHistory[lastAssistantMessageIndex]?.role === "assistant") {
targetHistory.splice(lastAssistantMessageIndex, 0, {
- role: "reasoning",
+ role: "thinking",
content: eventData.content,
html: DOMPurify.sanitize(marked.parse(eventData.content)),
image: [],
audio: [],
- expanded: false // Reasoning is always collapsed
+ expanded: shouldExpand
});
lastAssistantMessageIndex++; // Adjust index since we inserted
- // Scroll smoothly after adding reasoning
+ // Scroll smoothly after adding thinking
setTimeout(() => {
const chatContainer = document.getElementById('chat');
if (chatContainer) {
@@ -1425,7 +1431,7 @@ async function promptGPT(systemPrompt, input) {
}, 100);
} else {
// No assistant message yet, just add normally
- chatStore.add("reasoning", eventData.content, null, null, chatId);
+ chatStore.add("thinking", eventData.content, null, null, chatId);
}
}
break;
@@ -1491,14 +1497,17 @@ async function promptGPT(systemPrompt, input) {
// Only update display if this is the active chat (interval will handle it)
// Don't call updateTokensPerSecond here to avoid unnecessary updates
- // Check for thinking tags in the chunk (incremental detection)
- if (contentChunk.includes("") || contentChunk.includes("")) {
- isThinking = true;
- thinkingContent = "";
- lastThinkingMessageIndex = -1;
- }
-
- if (contentChunk.includes("") || contentChunk.includes("")) {
+ // Only check for thinking tags if we're NOT receiving reasoning from API
+ // This prevents duplicate thinking/reasoning messages
+ if (!hasReasoningFromAPI) {
+ // Check for thinking tags in the chunk (incremental detection)
+ if (contentChunk.includes("") || contentChunk.includes("")) {
+ isThinking = true;
+ thinkingContent = "";
+ lastThinkingMessageIndex = -1;
+ }
+
+ if (contentChunk.includes("") || contentChunk.includes("")) {
isThinking = false;
// When closing tag is detected, process the accumulated thinking content
if (thinkingContent.trim()) {
@@ -1552,10 +1561,11 @@ async function promptGPT(systemPrompt, input) {
}
thinkingContent = "";
}
+ }
}
- // Handle content based on thinking state
- if (isThinking) {
+ // Handle content based on thinking state (only if not receiving reasoning from API)
+ if (!hasReasoningFromAPI && isThinking) {
thinkingContent += contentChunk;
const currentChat = chatStore.getChat(chatId);
if (!currentChat) break; // Chat was deleted
@@ -1637,7 +1647,10 @@ async function promptGPT(systemPrompt, input) {
// Process any thinking tags that might be in the accumulated content
// This handles cases where tags are split across chunks
- const { regularContent: processedRegular, thinkingContent: processedThinking } = processThinkingTags(regularContent);
+ // Only process if we're NOT receiving reasoning from API (to avoid duplicates)
+ const { regularContent: processedRegular, thinkingContent: processedThinking } = hasReasoningFromAPI
+ ? { regularContent: regularContent, thinkingContent: "" }
+ : processThinkingTags(regularContent);
// Update or create assistant message with processed regular content
const currentChat = chatStore.getChat(chatId);
@@ -1645,10 +1658,10 @@ async function promptGPT(systemPrompt, input) {
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
if (lastAssistantMessageIndex === -1) {
- if (processedRegular && processedRegular.trim()) {
- chatStore.add("assistant", processedRegular, null, null, chatId, requestModel);
- lastAssistantMessageIndex = targetHistory.length - 1;
- }
+ // Create assistant message if we have any content (even if empty string after processing)
+ // This ensures the message is created and can be updated with more content later
+ chatStore.add("assistant", processedRegular || "", null, null, chatId, requestModel);
+ lastAssistantMessageIndex = targetHistory.length - 1;
} else {
const lastMessage = targetHistory[lastAssistantMessageIndex];
if (lastMessage && lastMessage.role === "assistant") {
@@ -1686,7 +1699,10 @@ async function promptGPT(systemPrompt, input) {
if (assistantContentBuffer.length > 0) {
const regularContent = assistantContentBuffer.join("");
// Process any remaining thinking tags that might be in the buffer
- const { regularContent: processedRegular, thinkingContent: processedThinking } = processThinkingTags(regularContent);
+ // Only process if we're NOT receiving reasoning from API (to avoid duplicates)
+ const { regularContent: processedRegular, thinkingContent: processedThinking } = hasReasoningFromAPI
+ ? { regularContent: regularContent, thinkingContent: "" }
+ : processThinkingTags(regularContent);
const currentChat = chatStore.getChat(chatId);
if (!currentChat) {
@@ -1719,23 +1735,26 @@ async function promptGPT(systemPrompt, input) {
}
// Then update or create assistant message
+ // Always create/update assistant message if we have any content
if (lastAssistantMessageIndex !== -1) {
const lastMessage = targetHistory[lastAssistantMessageIndex];
if (lastMessage && lastMessage.role === "assistant") {
lastMessage.content = (lastMessage.content || "") + (processedRegular || "");
lastMessage.html = DOMPurify.sanitize(marked.parse(lastMessage.content));
}
- } else if (processedRegular && processedRegular.trim()) {
+ } else {
+ // Create assistant message (even if empty, so it can be updated with more content)
const request = activeRequests.get(chatId);
const requestModel = request?.model || null;
- chatStore.add("assistant", processedRegular, null, null, chatId, requestModel);
+ chatStore.add("assistant", processedRegular || "", null, null, chatId, requestModel);
lastAssistantMessageIndex = targetHistory.length - 1;
}
}
// Final thinking content flush if any data remains (from incremental detection)
+ // Only process if we're NOT receiving reasoning from API (to avoid duplicates)
const finalChat = chatStore.getChat(chatId);
- if (finalChat && thinkingContent.trim() && lastThinkingMessageIndex === -1) {
+ if (finalChat && !hasReasoningFromAPI && thinkingContent.trim() && lastThinkingMessageIndex === -1) {
const finalHistory = finalChat.history;
// Extract thinking content if tags are present
const thinkingMatch = thinkingContent.match(/<(?:thinking|redacted_reasoning)>(.*?)<\/(?:thinking|redacted_reasoning)>/s);
@@ -1891,9 +1910,13 @@ async function promptGPT(systemPrompt, input) {
let buffer = "";
let contentBuffer = [];
let thinkingContent = "";
+ let reasoningContent = ""; // Track reasoning from API reasoning field
let isThinking = false;
let lastThinkingMessageIndex = -1;
+ let lastReasoningMessageIndex = -1; // Track reasoning message separately
+ let lastAssistantMessageIndex = -1; // Track assistant message for reasoning placement
let lastThinkingScrollTime = 0;
+ let hasReasoningFromAPI = false; // Track if we're receiving reasoning from API (skip tag-based detection)
const THINKING_SCROLL_THROTTLE = 200; // Throttle scrolling to every 200ms
try {
@@ -1929,30 +1952,100 @@ async function promptGPT(systemPrompt, input) {
chatStore.updateTokenUsage(jsonData.usage, chatId);
}
- const token = jsonData.choices[0].delta.content;
+ const token = jsonData.choices?.[0]?.delta?.content;
+ const reasoningDelta = jsonData.choices?.[0]?.delta?.reasoning;
- if (token) {
- // Check for thinking tags
- if (token.includes("") || token.includes("")) {
- isThinking = true;
- thinkingContent = "";
- lastThinkingMessageIndex = -1;
+ // Handle reasoning from API reasoning field - always use "thinking" role
+ if (reasoningDelta && reasoningDelta.trim() !== "") {
+ hasReasoningFromAPI = true; // Mark that we're receiving reasoning from API
+ reasoningContent += reasoningDelta;
+ const currentChat = chatStore.getChat(chatId);
+ if (!currentChat) {
+ // Chat was deleted, skip this line
return;
}
- if (token.includes("") || token.includes("")) {
- isThinking = false;
- if (thinkingContent.trim()) {
- // Only add the final thinking message if we don't already have one
- if (lastThinkingMessageIndex === -1) {
- chatStore.add("thinking", thinkingContent, null, null, chatId);
+ const isMCPMode = currentChat.mcpMode || false;
+ const shouldExpand = !isMCPMode; // Expanded in non-MCP mode, collapsed in MCP mode
+
+ // Only create/update thinking message if we have actual content
+ if (reasoningContent.trim() !== "") {
+ // Update or create thinking message (always use "thinking" role, not "reasoning")
+ if (lastReasoningMessageIndex === -1) {
+ // Find the last assistant message index to insert thinking before it
+ const targetHistory = currentChat.history;
+ const assistantIndex = targetHistory.length - 1;
+ if (assistantIndex >= 0 && targetHistory[assistantIndex]?.role === "assistant") {
+ // Insert thinking before assistant message
+ targetHistory.splice(assistantIndex, 0, {
+ role: "thinking",
+ content: reasoningContent,
+ html: DOMPurify.sanitize(marked.parse(reasoningContent)),
+ image: [],
+ audio: [],
+ expanded: shouldExpand
+ });
+ lastReasoningMessageIndex = assistantIndex;
+ lastAssistantMessageIndex = assistantIndex + 1; // Adjust for inserted thinking
+ } else {
+ // No assistant message yet, just add normally
+ chatStore.add("thinking", reasoningContent, null, null, chatId);
+ lastReasoningMessageIndex = currentChat.history.length - 1;
+ }
+ } else {
+ // Update existing thinking message
+ const targetHistory = currentChat.history;
+ if (lastReasoningMessageIndex >= 0 && lastReasoningMessageIndex < targetHistory.length) {
+ const thinkingMessage = targetHistory[lastReasoningMessageIndex];
+ if (thinkingMessage && thinkingMessage.role === "thinking") {
+ thinkingMessage.content = reasoningContent;
+ thinkingMessage.html = DOMPurify.sanitize(marked.parse(reasoningContent));
+ }
}
}
- return;
}
+
+ // Scroll when reasoning is updated (throttled)
+ const now = Date.now();
+ if (now - lastThinkingScrollTime > THINKING_SCROLL_THROTTLE) {
+ lastThinkingScrollTime = now;
+ setTimeout(() => {
+ const chatContainer = document.getElementById('chat');
+ if (chatContainer) {
+ chatContainer.scrollTo({
+ top: chatContainer.scrollHeight,
+ behavior: 'smooth'
+ });
+ }
+ scrollThinkingBoxToBottom();
+ }, 100);
+ }
+ }
- // Handle content based on thinking state
- if (isThinking) {
- thinkingContent += token;
+ if (token && token.trim() !== "") {
+ // Only check for thinking tags if we're NOT receiving reasoning from API
+ // This prevents duplicate thinking/reasoning messages
+ if (!hasReasoningFromAPI) {
+ // Check for thinking tags (legacy support - models that output tags directly)
+ if (token.includes("") || token.includes("")) {
+ isThinking = true;
+ thinkingContent = "";
+ lastThinkingMessageIndex = -1;
+ return;
+ }
+ if (token.includes("") || token.includes("")) {
+ isThinking = false;
+ if (thinkingContent.trim()) {
+ // Only add the final thinking message if we don't already have one
+ if (lastThinkingMessageIndex === -1) {
+ chatStore.add("thinking", thinkingContent, null, null, chatId);
+ }
+ }
+ return;
+ }
+
+ // Handle content based on thinking state
+ if (isThinking) {
+ thinkingContent += token;
// Count tokens for rate calculation (per chat)
const request = activeRequests.get(chatId);
if (request) {
@@ -1995,7 +2088,42 @@ async function promptGPT(systemPrompt, input) {
}, 100);
}
} else {
+ // Not in thinking state, add to content buffer
contentBuffer.push(token);
+ // Track assistant message index for reasoning placement
+ if (lastAssistantMessageIndex === -1) {
+ const currentChat = chatStore.getChat(chatId);
+ if (currentChat) {
+ const targetHistory = currentChat.history;
+ // Find or create assistant message index
+ for (let i = targetHistory.length - 1; i >= 0; i--) {
+ if (targetHistory[i].role === "assistant") {
+ lastAssistantMessageIndex = i;
+ break;
+ }
+ }
+ // If no assistant message yet, it will be created when we flush contentBuffer
+ }
+ }
+ }
+ } else {
+ // Receiving reasoning from API, just add token to content buffer
+ contentBuffer.push(token);
+ // Track assistant message index for reasoning placement
+ if (lastAssistantMessageIndex === -1) {
+ const currentChat = chatStore.getChat(chatId);
+ if (currentChat) {
+ const targetHistory = currentChat.history;
+ // Find or create assistant message index
+ for (let i = targetHistory.length - 1; i >= 0; i--) {
+ if (targetHistory[i].role === "assistant") {
+ lastAssistantMessageIndex = i;
+ break;
+ }
+ }
+ // If no assistant message yet, it will be created when we flush contentBuffer
+ }
+ }
}
}
} catch (error) {
@@ -2007,6 +2135,17 @@ async function promptGPT(systemPrompt, input) {
// Efficiently update the chat in batch
if (contentBuffer.length > 0) {
addToChat(contentBuffer.join(""));
+ // Update assistant message index after adding content
+ const currentChat = chatStore.getChat(chatId);
+ if (currentChat) {
+ const targetHistory = currentChat.history;
+ for (let i = targetHistory.length - 1; i >= 0; i--) {
+ if (targetHistory[i].role === "assistant") {
+ lastAssistantMessageIndex = i;
+ break;
+ }
+ }
+ }
contentBuffer = [];
// Scroll when assistant content is updated (this will also show thinking messages above)
setTimeout(() => {
@@ -2025,7 +2164,30 @@ async function promptGPT(systemPrompt, input) {
if (contentBuffer.length > 0) {
addToChat(contentBuffer.join(""));
}
+
+ // Final reasoning flush if any data remains - always use "thinking" role
const finalChat = chatStore.getChat(chatId);
+ if (finalChat && reasoningContent.trim() && lastReasoningMessageIndex === -1) {
+ const isMCPMode = finalChat.mcpMode || false;
+ const shouldExpand = !isMCPMode;
+ const targetHistory = finalChat.history;
+ // Find assistant message to insert before
+ const assistantIndex = targetHistory.length - 1;
+ if (assistantIndex >= 0 && targetHistory[assistantIndex]?.role === "assistant") {
+ targetHistory.splice(assistantIndex, 0, {
+ role: "thinking",
+ content: reasoningContent,
+ html: DOMPurify.sanitize(marked.parse(reasoningContent)),
+ image: [],
+ audio: [],
+ expanded: shouldExpand
+ });
+ } else {
+ chatStore.add("thinking", reasoningContent, null, null, chatId);
+ }
+ }
+
+ // Final thinking content flush (legacy tag-based thinking)
if (finalChat && thinkingContent.trim() && lastThinkingMessageIndex === -1) {
chatStore.add("thinking", thinkingContent, null, null, chatId);
}
diff --git a/core/http/views/chat.html b/core/http/views/chat.html
index dbb4dc862..6c064977c 100644
--- a/core/http/views/chat.html
+++ b/core/http/views/chat.html
@@ -41,7 +41,7 @@ SOFTWARE.
__chatContextSize = {{ .ContextSize }};
{{ end }}
- // Store gallery configs for header icon display
+ // Store gallery configs for header icon display and model info modal
window.__galleryConfigs = {};
{{ $allGalleryConfigs:=.GalleryConfig }}
{{ range $modelName, $galleryConfig := $allGalleryConfigs }}
@@ -49,6 +49,16 @@ SOFTWARE.
{{ if $galleryConfig.Icon }}
window.__galleryConfigs["{{$modelName}}"].Icon = "{{$galleryConfig.Icon}}";
{{ end }}
+ {{ if $galleryConfig.Description }}
+ window.__galleryConfigs["{{$modelName}}"].Description = {{ printf "%q" $galleryConfig.Description }};
+ {{ end }}
+ {{ if $galleryConfig.URLs }}
+ window.__galleryConfigs["{{$modelName}}"].URLs = [
+ {{ range $idx, $url := $galleryConfig.URLs }}
+ {{ if $idx }},{{ end }}{{ printf "%q" $url }}
+ {{ end }}
+ ];
+ {{ end }}
{{ end }}
// Function to initialize store
@@ -326,10 +336,10 @@ SOFTWARE.
c += DOMPurify.sanitize(marked.parse(line));
});
}
- // Set expanded state: thinking is expanded by default in non-MCP mode, collapsed in MCP mode
- // Reasoning, tool_call, and tool_result are always collapsed by default
+ // Set expanded state: thinking and reasoning are expanded by default in non-MCP mode, collapsed in MCP mode
+ // tool_call and tool_result are always collapsed by default
const isMCPMode = chat.mcpMode || false;
- const shouldExpand = (role === "thinking" && !isMCPMode) || false;
+ const shouldExpand = ((role === "thinking" || role === "reasoning") && !isMCPMode) || false;
chat.history.push({ role, content, html: c, image, audio, expanded: shouldExpand, model: messageModel });
// Auto-name chat from first user message
@@ -497,6 +507,11 @@ SOFTWARE.
activeChat.model = modelName;
activeChat.updatedAt = Date.now();
+ // Update model info modal with new model
+ if (window.updateModelInfoModal) {
+ window.updateModelInfoModal(modelName);
+ }
+
// Get context size from data attribute
let contextSize = null;
if (selectedOption.dataset.contextSize) {
@@ -536,18 +551,23 @@ SOFTWARE.
}
// Update model selector to reflect the change (ensure it stays in sync)
+ // Note: We don't dispatch a change event here to avoid infinite loop
+ // The selector is already updated via user interaction or programmatic change
const modelSelector = document.getElementById('modelSelector');
if (modelSelector) {
// Find and select the option matching the model
const optionValue = 'chat/' + modelName;
for (let i = 0; i < modelSelector.options.length; i++) {
if (modelSelector.options[i].value === optionValue) {
- modelSelector.selectedIndex = i;
+ // Only update if it's different to avoid unnecessary updates
+ if (modelSelector.selectedIndex !== i) {
+ modelSelector.selectedIndex = i;
+ }
break;
}
}
- // Trigger Alpine reactivity by dispatching change event
- modelSelector.dispatchEvent(new Event('change', { bubbles: true }));
+ // Don't dispatch change event here - it would cause infinite recursion
+ // The selector is already in sync with the model
}
// Trigger MCP availability check in Alpine component
@@ -603,27 +623,52 @@ SOFTWARE.
- {{ if $model }}
- {{ $galleryConfig:= index $allGalleryConfigs $model}}
- {{ if $galleryConfig }}
-
- {{ end }}
- {{ end }}
- {{ if $model }}
-
-
-
- {{ end }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ if $model }}
+
+
+
+ {{ end }}
+