Skip to content

Commit f91120f

Browse files
committed
feat: Implement reasoning_content
1 parent 31035bc commit f91120f

File tree

7 files changed

+78
-39
lines changed

7 files changed

+78
-39
lines changed

tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@
5050
let messageContent = $derived.by(() => {
5151
if (message.role === 'assistant') {
5252
const parsed = parseThinkingContent(message.content);
53-
return parsed.cleanContent;
53+
return parsed.cleanContent?.replace('<|channel|>analysis', '');
5454
}
55-
return message.content;
55+
56+
return message.content?.replace('<|channel|>analysis', '');
5657
});
5758
5859
async function handleCopy() {
@@ -174,7 +175,25 @@
174175
aria-label="Assistant message with actions"
175176
>
176177
{#if thinkingContent}
177-
<ChatMessageThinkingBlock thinking={thinkingContent} isStreaming={!message.timestamp} />
178+
<ChatMessageThinkingBlock reasoningContent={thinkingContent} isStreaming={!message.timestamp} />
179+
{/if}
180+
181+
{#if message?.role === 'assistant' && !message.content && isLoading()}
182+
<div class="w-full max-w-[48rem] mt-6" in:fade>
183+
<div class="processing-container">
184+
<span class="processing-text">
185+
{processingState.getProcessingMessage()}
186+
</span>
187+
188+
{#if processingState.shouldShowDetails()}
189+
<div class="processing-details">
190+
{#each processingState.getProcessingDetails() as detail}
191+
<span class="processing-detail">{detail}</span>
192+
{/each}
193+
</div>
194+
{/if}
195+
</div>
196+
</div>
178197
{/if}
179198

180199
{#if message.role === 'assistant'}
@@ -194,24 +213,6 @@
194213
{/if}
195214

196215
{#snippet messageActions(config?: { role: ChatRole })}
197-
{#if config?.role === 'assistant' && !message.content && isLoading()}
198-
<div class="w-full max-w-[48rem] mb-24" in:fade>
199-
<div class="processing-container">
200-
<span class="processing-text">
201-
{processingState.getProcessingMessage()}
202-
</span>
203-
204-
{#if processingState.shouldShowDetails()}
205-
<div class="processing-details">
206-
{#each processingState.getProcessingDetails() as detail}
207-
<span class="processing-detail">{detail}</span>
208-
{/each}
209-
</div>
210-
{/if}
211-
</div>
212-
</div>
213-
{/if}
214-
215216
<div
216217
class="pointer-events-none inset-0 flex items-center gap-1 opacity-0 transition-all duration-150 group-hover:pointer-events-auto group-hover:opacity-100"
217218
>

tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageThinkingBlock.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import { slide } from 'svelte/transition';
77
88
interface Props {
9-
thinking: string | null;
9+
reasoningContent: string | null;
1010
isStreaming?: boolean;
1111
class?: string;
1212
}
1313
14-
let { thinking, isStreaming = false, class: className = '' }: Props = $props();
14+
let { reasoningContent, isStreaming = false, class: className = '' }: Props = $props();
1515
1616
let isExpanded = $state(false);
1717
</script>
@@ -26,7 +26,7 @@
2626
<Brain class="h-4 w-4" />
2727

2828
<span class="text-sm">
29-
{isStreaming ? 'Thinking...' : 'Thinking summary'}
29+
{isStreaming ? 'Reasoning...' : 'Reasoning'}
3030
</span>
3131
</div>
3232

@@ -40,7 +40,7 @@
4040
{#if isExpanded}
4141
<div class="border-muted border-t px-3 pb-3" transition:slide={{ duration: 200 }}>
4242
<div class="pt-3">
43-
<MarkdownContent content={thinking || ''} class="text-xs leading-relaxed" />
43+
<MarkdownContent content={reasoningContent || ''} class="text-xs leading-relaxed" />
4444
</div>
4545
</div>
4646
{/if}

tools/server/webui/src/lib/services/chat.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class ChatService {
1919
async sendMessage(
2020
messages: ApiChatMessageData[],
2121
options: SettingsChatServiceOptions = {}
22-
): Promise<string | void> {
22+
): Promise<string | void> {
2323
const {
2424
stream, onChunk, onComplete, onError,
2525
// Generation parameters
@@ -45,6 +45,7 @@ export class ChatService {
4545
role: msg.role,
4646
content: msg.content
4747
})),
48+
reasoning_format: 'auto',
4849
stream
4950
};
5051

@@ -124,7 +125,7 @@ export class ChatService {
124125
}
125126

126127
if (stream) {
127-
return this.handleStreamResponse(response, onChunk, onComplete, onError);
128+
return this.handleStreamResponse(response, onChunk, onComplete, onError, options.onReasoningChunk);
128129
} else {
129130
return this.handleNonStreamResponse(response, onComplete, onError);
130131
}
@@ -166,14 +167,16 @@ export class ChatService {
166167
* @param onChunk - Optional callback invoked for each content chunk received
167168
* @param onComplete - Optional callback invoked when the stream is complete with full response
168169
* @param onError - Optional callback invoked if an error occurs during streaming
170+
* @param onReasoningChunk - Optional callback invoked for each reasoning content chunk
169171
* @returns {Promise<void>} Promise that resolves when streaming is complete
170172
* @throws {Error} if the stream cannot be read or parsed
171173
*/
172174
private async handleStreamResponse(
173175
response: Response,
174176
onChunk?: (chunk: string) => void,
175-
onComplete?: (response: string) => void,
176-
onError?: (error: Error) => void
177+
onComplete?: (response: string, reasoningContent?: string) => void,
178+
onError?: (error: Error) => void,
179+
onReasoningChunk?: (chunk: string) => void
177180
): Promise<void> {
178181
const reader = response.body?.getReader();
179182

@@ -183,6 +186,7 @@ export class ChatService {
183186

184187
const decoder = new TextDecoder();
185188
let fullResponse = '';
189+
let fullReasoningContent = '';
186190
let thinkContent = '';
187191
let regularContent = '';
188192
let insideThinkTag = false;
@@ -208,13 +212,15 @@ export class ChatService {
208212
onError?.(contextError);
209213
return;
210214
}
211-
onComplete?.(regularContent);
215+
onComplete?.(regularContent, fullReasoningContent || undefined);
212216
return;
213217
}
214218

215219
try {
216220
const parsed: ApiChatCompletionStreamChunk = JSON.parse(data);
217221
const content = parsed.choices[0]?.delta?.content;
222+
const reasoningContent = parsed.choices[0]?.delta?.reasoning_content;
223+
218224
if (content) {
219225
hasReceivedData = true;
220226
fullResponse += content;
@@ -240,6 +246,12 @@ export class ChatService {
240246
onChunk?.(newRegularContent);
241247
}
242248
}
249+
250+
if (reasoningContent) {
251+
hasReceivedData = true;
252+
fullReasoningContent += reasoningContent;
253+
onReasoningChunk?.(reasoningContent);
254+
}
243255
} catch (e) {
244256
console.error('Error parsing JSON chunk:', e);
245257
}
@@ -272,14 +284,14 @@ export class ChatService {
272284
* @param response - The fetch Response object containing the JSON data
273285
* @param onComplete - Optional callback invoked when response is successfully parsed
274286
* @param onError - Optional callback invoked if an error occurs during parsing
275-
* @returns {Promise<string>} Promise that resolves to the generated content string
287+
* @returns {Promise<string>} Promise that resolves to the generated content string
276288
* @throws {Error} if the response cannot be parsed or is malformed
277289
*/
278290
private async handleNonStreamResponse(
279291
response: Response,
280-
onComplete?: (response: string) => void,
292+
onComplete?: (response: string, reasoningContent?: string) => void,
281293
onError?: (error: Error) => void
282-
): Promise<string> {
294+
): Promise<string> {
283295
try {
284296
// Check if response body is empty
285297
const responseText = await response.text();
@@ -293,6 +305,11 @@ export class ChatService {
293305

294306
const data: ApiChatCompletionResponse = JSON.parse(responseText);
295307
const content = data.choices[0]?.message?.content || '';
308+
const reasoningContent = data.choices[0]?.message?.reasoning_content;
309+
310+
if (reasoningContent) {
311+
console.log('Full reasoning content:', reasoningContent);
312+
}
296313

297314
// Check if content is empty even with valid JSON structure
298315
if (!content.trim()) {
@@ -302,7 +319,7 @@ export class ChatService {
302319
throw contextError;
303320
}
304321

305-
onComplete?.(content);
322+
onComplete?.(content, reasoningContent);
306323

307324
return content;
308325
} catch (error) {
@@ -432,7 +449,8 @@ export class ChatService {
432449
temperature?: number;
433450
max_tokens?: number;
434451
onChunk?: (chunk: string) => void;
435-
onComplete?: (response?: string) => void;
452+
onReasoningChunk?: (chunk: string) => void;
453+
onComplete?: (response?: string, reasoningContent?: string) => void;
436454
onError?: (error: Error) => void;
437455
} = {}
438456
): Promise<string | void> {

tools/server/webui/src/lib/stores/chat.svelte.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ class ChatStore {
162162
custom: currentConfig.custom || '',
163163
};
164164

165+
let streamedReasoningContent = '';
166+
165167
await this.chatService.sendChatCompletion(
166168
allMessages,
167169
{
@@ -182,10 +184,23 @@ class ChatStore {
182184
this.activeMessages[messageIndex].content = partialThinking.remainingContent || streamedContent;
183185
}
184186
},
185-
onComplete: async () => {
187+
onReasoningChunk: (reasoningChunk: string) => {
188+
streamedReasoningContent += reasoningChunk;
189+
190+
const messageIndex = this.activeMessages.findIndex(
191+
(m) => m.id === assistantMessage.id
192+
);
193+
194+
if (messageIndex !== -1) {
195+
// Update message with reasoning content
196+
this.activeMessages[messageIndex].thinking = streamedReasoningContent;
197+
}
198+
},
199+
onComplete: async (finalContent?: string, reasoningContent?: string) => {
186200
// Update assistant message in database
187201
await DatabaseService.updateMessage(assistantMessage.id, {
188-
content: streamedContent
202+
content: finalContent || streamedContent,
203+
thinking: reasoningContent || streamedReasoningContent
189204
});
190205

191206
// Call custom completion handler if provided

tools/server/webui/src/lib/types/api.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ export interface ApiChatCompletionRequest {
100100
content: string | ApiChatMessageContentPart[];
101101
}>;
102102
stream?: boolean;
103+
// Reasoning parameters
104+
reasoning_format?: string;
103105
// Generation parameters
104106
temperature?: number;
105107
max_tokens?: number;
@@ -131,6 +133,7 @@ export interface ApiChatCompletionStreamChunk {
131133
choices: Array<{
132134
delta: {
133135
content?: string;
136+
reasoning_content?: string;
134137
};
135138
}>;
136139
timings?: {
@@ -145,6 +148,7 @@ export interface ApiChatCompletionResponse {
145148
choices: Array<{
146149
message: {
147150
content: string;
151+
reasoning_content?: string;
148152
};
149153
}>;
150154
}

tools/server/webui/src/lib/types/settings.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export interface SettingsChatServiceOptions {
3838
custom?: any;
3939
// Callbacks
4040
onChunk?: (chunk: string) => void;
41-
onComplete?: (response: string) => void;
41+
onReasoningChunk?: (chunk: string) => void;
42+
onComplete?: (response: string, reasoningContent?: string) => void;
4243
onError?: (error: Error) => void;
4344
}
4445

tools/server/webui/src/lib/utils/thinking.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function parseThinkingContent(content: string): {
4444
* @returns True if the content contains an opening <think> tag
4545
*/
4646
export function hasThinkingStart(content: string): boolean {
47-
return content.includes('<think>');
47+
return content.includes('<think>') || content.includes('<|channel|>analysis');
4848
}
4949

5050
/**

0 commit comments

Comments
 (0)