diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageThinkingBlock.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageThinkingBlock.svelte index cd7ad69769..6c51176aad 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageThinkingBlock.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageThinkingBlock.svelte @@ -5,19 +5,22 @@ import { buttonVariants } from '$lib/components/ui/button/index.js'; import { Card } from '$lib/components/ui/card'; import { config } from '$lib/stores/settings.svelte'; + import type { Snippet } from 'svelte'; interface Props { class?: string; hasRegularContent?: boolean; isStreaming?: boolean; reasoningContent: string | null; + children?: Snippet; } let { class: className = '', hasRegularContent = false, isStreaming = false, - reasoningContent + reasoningContent, + children }: Props = $props(); const currentConfig = config(); @@ -72,9 +75,11 @@
- + {#if children} + {@render children()} + {:else} {reasoningContent ?? ''} - + {/if}
diff --git a/tools/server/webui/src/lib/services/chat.ts b/tools/server/webui/src/lib/services/chat.ts index 3c616fc1dd..763c539dd0 100644 --- a/tools/server/webui/src/lib/services/chat.ts +++ b/tools/server/webui/src/lib/services/chat.ts @@ -119,6 +119,7 @@ export class ChatService { messages: normalizedMessages.map((msg: ApiChatMessageData) => ({ role: msg.role, content: msg.content, + ...(msg.reasoning_content ? { reasoning_content: msg.reasoning_content } : {}), ...((msg as ApiChatCompletionRequestMessage).tool_call_id ? { tool_call_id: (msg as ApiChatCompletionRequestMessage).tool_call_id } : {}), @@ -602,6 +603,9 @@ export class ChatService { return { role: message.role as ChatRole, content: message.content, + ...(message.role === 'assistant' && message.thinking + ? { reasoning_content: message.thinking } + : {}), // tool_call_id is only relevant for tool role messages ...(message.toolCallId ? { tool_call_id: message.toolCallId } : {}), ...(toolCalls ? { tool_calls: toolCalls } : {}) @@ -693,6 +697,9 @@ export class ChatService { return { role: message.role as ChatRole, content: contentParts, + ...(message.role === 'assistant' && message.thinking + ? { reasoning_content: message.thinking } + : {}), ...(message.toolCallId ? { tool_call_id: message.toolCallId } : {}), ...(toolCalls ? { tool_calls: toolCalls } : {}) }; diff --git a/tools/server/webui/src/lib/types/api.d.ts b/tools/server/webui/src/lib/types/api.d.ts index f507a7de50..9f5b1d19d0 100644 --- a/tools/server/webui/src/lib/types/api.d.ts +++ b/tools/server/webui/src/lib/types/api.d.ts @@ -35,6 +35,13 @@ export interface ApiChatMessageData { role: ChatRole; content: string | ApiChatMessageContentPart[]; timestamp?: number; + /** + * Optional reasoning/thinking content to be sent back to the server. + * + * llama-server accepts this non-OpenAI field and uses it to preserve the model's + * internal "thinking" blocks across tool-call resumptions (notably for gpt-oss). + */ + reasoning_content?: string; tool_call_id?: string; tool_calls?: ApiChatCompletionToolCallDelta[]; } @@ -183,6 +190,7 @@ export interface ApiLlamaCppServerProps { export interface ApiChatCompletionRequestMessage { role: ChatRole; content: string | ApiChatMessageContentPart[]; + reasoning_content?: string; tool_call_id?: string; tool_calls?: ApiChatCompletionToolCallDelta[]; } diff --git a/tools/server/webui/tests/client/chatMessage.delete-merged-assistant.test.ts b/tools/server/webui/tests/client/chatMessage.delete-merged-assistant.test.ts index 19dee42f1d..1b5c2b3606 100644 --- a/tools/server/webui/tests/client/chatMessage.delete-merged-assistant.test.ts +++ b/tools/server/webui/tests/client/chatMessage.delete-merged-assistant.test.ts @@ -75,9 +75,8 @@ describe('ChatMessage delete for merged assistant messages', () => { conversationsStore.activeMessages = allMessages; // Avoid touching IndexedDB by stubbing the store call used by getDeletionInfo. - const originalGetConversationMessages = conversationsStore.getConversationMessages.bind( - conversationsStore - ); + const originalGetConversationMessages = + conversationsStore.getConversationMessages.bind(conversationsStore); conversationsStore.getConversationMessages = async () => allMessages; const onDelete = vi.fn(); @@ -111,4 +110,3 @@ describe('ChatMessage delete for merged assistant messages', () => { } }); }); - diff --git a/tools/server/webui/tests/client/components/TestChatMessageWrapper.svelte b/tools/server/webui/tests/client/components/TestChatMessageWrapper.svelte index 544a881a54..9ae5505b02 100644 --- a/tools/server/webui/tests/client/components/TestChatMessageWrapper.svelte +++ b/tools/server/webui/tests/client/components/TestChatMessageWrapper.svelte @@ -14,4 +14,3 @@ - diff --git a/tools/server/webui/tests/e2e/tool-output-no-echo.spec.ts b/tools/server/webui/tests/e2e/tool-output-no-echo.spec.ts index e59f6d3e09..8f03257930 100644 --- a/tools/server/webui/tests/e2e/tool-output-no-echo.spec.ts +++ b/tools/server/webui/tests/e2e/tool-output-no-echo.spec.ts @@ -137,4 +137,6 @@ test('tool output does not echo tool arguments back to the model', async ({ page ); expect(assistantWithToolCall).toBeTruthy(); expect(JSON.stringify(assistantWithToolCall?.tool_calls ?? null)).toContain('LARGE_CODE_BEGIN'); + // Preserve the model's reasoning across tool-call resumptions (required for gpt-oss). + expect(String(assistantWithToolCall?.reasoning_content ?? '')).toContain('reasoning-step-1'); });