server, webui: support continue generation on reasoning models (#22727)
* server, webui : support continue generation on reasoning models (#22727) Remove the throw blocking assistant prefill on reasoning models and orchestrate thinking tags around the prefilled message so the parser routes the next stream chunks correctly. WebUI drops the reasoning guard on the Continue button, sends reasoning_content with the prefilled message and persists partial reasoning on stop so the CoT survives reload and resume. Scope : templates with a simple thinking_start_tag / thinking_end_tag pair. Channel-based templates like GPT-OSS are out of scope, pending a per-template prefill API in common/chat. First step toward #21754. * chore: update webui build output * server: reject reasoning prefill on channel based templates
This commit is contained in:
+1
-2
@@ -74,7 +74,6 @@
|
||||
const editCtx = getMessageEditContext();
|
||||
|
||||
const isAgentic = $derived(hasAgenticContent(message, toolMessages));
|
||||
const hasReasoning = $derived(!!message.reasoningContent);
|
||||
const processingState = useProcessingState();
|
||||
|
||||
let currentConfig = $derived(config());
|
||||
@@ -329,7 +328,7 @@
|
||||
{onCopy}
|
||||
{onEdit}
|
||||
{onRegenerate}
|
||||
onContinue={currentConfig.enableContinueGeneration && !hasReasoning ? onContinue : undefined}
|
||||
onContinue={currentConfig.enableContinueGeneration ? onContinue : undefined}
|
||||
{onForkConversation}
|
||||
{onDelete}
|
||||
{onConfirmDelete}
|
||||
|
||||
@@ -122,7 +122,7 @@ const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
|
||||
{
|
||||
key: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION,
|
||||
label: 'Enable "Continue" button',
|
||||
help: 'Enable "Continue" button for assistant messages. Currently works only with non-reasoning models.',
|
||||
help: 'Enable "Continue" button for assistant messages, including reasoning models.',
|
||||
defaultValue: false,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||
|
||||
@@ -674,7 +674,8 @@ class ChatStore {
|
||||
},
|
||||
onReasoningChunk: (chunk: string) => {
|
||||
streamedReasoningContent += chunk;
|
||||
// Update UI to show reasoning is being received
|
||||
// mark streaming state so a stop mid-thinking can persist the partial reasoning
|
||||
this.setChatStreaming(convId, streamedContent, currentMessageId);
|
||||
const idx = conversationsStore.findMessageIndex(currentMessageId);
|
||||
conversationsStore.updateMessageAtIndex(idx, {
|
||||
reasoningContent: streamedReasoningContent
|
||||
@@ -989,38 +990,51 @@ class ChatStore {
|
||||
const conversationId = convId || conversationsStore.activeConversation?.id;
|
||||
if (!conversationId) return;
|
||||
const streamingState = this.getChatStreaming(conversationId);
|
||||
if (!streamingState || !streamingState.response.trim()) return;
|
||||
if (!streamingState) return;
|
||||
const messages =
|
||||
conversationId === conversationsStore.activeConversation?.id
|
||||
? conversationsStore.activeMessages
|
||||
: await conversationsStore.getConversationMessages(conversationId);
|
||||
if (!messages.length) return;
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (lastMessage?.role === MessageRole.ASSISTANT) {
|
||||
try {
|
||||
const updateData: { content: string; timings?: ChatMessageTimings } = {
|
||||
content: streamingState.response
|
||||
};
|
||||
const lastKnownState = this.getProcessingState(conversationId);
|
||||
if (lastKnownState) {
|
||||
updateData.timings = {
|
||||
prompt_n: lastKnownState.promptTokens || 0,
|
||||
prompt_ms: lastKnownState.promptMs,
|
||||
predicted_n: lastKnownState.tokensDecoded || 0,
|
||||
cache_n: lastKnownState.cacheTokens || 0,
|
||||
predicted_ms:
|
||||
lastKnownState.tokensPerSecond && lastKnownState.tokensDecoded
|
||||
? (lastKnownState.tokensDecoded / lastKnownState.tokensPerSecond) * 1000
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
await DatabaseService.updateMessage(lastMessage.id, updateData);
|
||||
lastMessage.content = streamingState.response;
|
||||
if (updateData.timings) lastMessage.timings = updateData.timings;
|
||||
} catch (error) {
|
||||
lastMessage.content = streamingState.response;
|
||||
console.error('Failed to save partial response:', error);
|
||||
if (lastMessage?.role !== MessageRole.ASSISTANT) return;
|
||||
|
||||
const partialContent = streamingState.response;
|
||||
const partialReasoning = lastMessage.reasoningContent || '';
|
||||
|
||||
// nothing to persist when both content and reasoning are empty (e.g. stop before any token)
|
||||
if (!partialContent.trim() && !partialReasoning.trim()) return;
|
||||
|
||||
try {
|
||||
const updateData: {
|
||||
content: string;
|
||||
reasoningContent?: string;
|
||||
timings?: ChatMessageTimings;
|
||||
} = {
|
||||
content: partialContent
|
||||
};
|
||||
if (partialReasoning) {
|
||||
updateData.reasoningContent = partialReasoning;
|
||||
}
|
||||
const lastKnownState = this.getProcessingState(conversationId);
|
||||
if (lastKnownState) {
|
||||
updateData.timings = {
|
||||
prompt_n: lastKnownState.promptTokens || 0,
|
||||
prompt_ms: lastKnownState.promptMs,
|
||||
predicted_n: lastKnownState.tokensDecoded || 0,
|
||||
cache_n: lastKnownState.cacheTokens || 0,
|
||||
predicted_ms:
|
||||
lastKnownState.tokensPerSecond && lastKnownState.tokensDecoded
|
||||
? (lastKnownState.tokensDecoded / lastKnownState.tokensPerSecond) * 1000
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
await DatabaseService.updateMessage(lastMessage.id, updateData);
|
||||
lastMessage.content = partialContent;
|
||||
if (updateData.timings) lastMessage.timings = updateData.timings;
|
||||
} catch (error) {
|
||||
lastMessage.content = partialContent;
|
||||
console.error('Failed to save partial response:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1265,7 +1279,11 @@ class ChatStore {
|
||||
const conversationContext = conversationsStore.activeMessages.slice(0, idx);
|
||||
const contextWithContinue = [
|
||||
...conversationContext,
|
||||
{ role: MessageRole.ASSISTANT as const, content: originalContent }
|
||||
{
|
||||
role: MessageRole.ASSISTANT as const,
|
||||
content: originalContent,
|
||||
reasoning_content: originalReasoning || undefined
|
||||
}
|
||||
];
|
||||
|
||||
let appendedContent = '';
|
||||
@@ -1291,6 +1309,8 @@ class ChatStore {
|
||||
onReasoningChunk: (chunk: string) => {
|
||||
appendedReasoning += chunk;
|
||||
hasReceivedContent = true;
|
||||
// mark streaming state so a stop mid-thinking can persist the partial reasoning
|
||||
this.setChatStreaming(msg.convId, originalContent + appendedContent, msg.id);
|
||||
conversationsStore.updateMessageAtIndex(idx, {
|
||||
reasoningContent: originalReasoning + appendedReasoning
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user