mirror of
https://github.com/LostRuins/koboldcpp.git
synced 2026-09-19 09:15:18 +02:00
Merge branch 'upstream' into concedo_experimental
# Conflicts: # .devops/intel.Dockerfile # .github/workflows/build-cross.yml # .github/workflows/build-sycl.yml # .github/workflows/build.yml # .github/workflows/editorconfig.yml # .github/workflows/release.yml # cmake/riscv64-spacemit-linux-gnu-gcc.cmake # docs/backend/OPENVINO.md # docs/backend/SYCL.md # docs/build-riscv64-spacemit.md # docs/ops.md # docs/ops/WebGPU.csv # embd_res/ggml-vocab-qwen35.gguf # embd_res/ggml-vocab-qwen35.gguf.inp # embd_res/ggml-vocab-qwen35.gguf.out # examples/model-conversion/Makefile # ggml/CMakeLists.txt # ggml/src/ggml-cpu/CMakeLists.txt # ggml/src/ggml-hexagon/ggml-hexagon.cpp # ggml/src/ggml-hexagon/htp/hmx-flash-attn-ops.c # ggml/src/ggml-hexagon/htp/hmx-matmul-ops.c # ggml/src/ggml-hexagon/htp/hmx-utils.h # ggml/src/ggml-hexagon/htp/htp-ops.h # ggml/src/ggml-hexagon/htp/hvx-utils.h # ggml/src/ggml-hexagon/htp/main.c # ggml/src/ggml-hexagon/htp/unary-ops.c # ggml/src/ggml-opencl/CMakeLists.txt # ggml/src/ggml-opencl/ggml-opencl.cpp # ggml/src/ggml-opencl/kernels/cvt.cl # ggml/src/ggml-sycl/CMakeLists.txt # ggml/src/ggml-sycl/common.cpp # ggml/src/ggml-sycl/common.hpp # ggml/src/ggml-sycl/ggml-sycl.cpp # ggml/src/ggml-webgpu/ggml-webgpu-shader-lib.hpp # ggml/src/ggml-webgpu/ggml-webgpu.cpp # ggml/src/ggml-webgpu/wgsl-shaders/common_decls.tmpl # ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_tile.wgsl # ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_vec_reduce.wgsl # ggml/src/ggml-webgpu/wgsl-shaders/flash_attn_vec_split.wgsl # ggml/src/ggml-webgpu/wgsl-shaders/get_rows.wgsl # ggml/src/ggml-webgpu/wgsl-shaders/mul_mat_decls.tmpl # ggml/src/ggml-webgpu/wgsl-shaders/mul_mat_vec_acc.tmpl # ggml/src/ggml-webgpu/wgsl-shaders/unary.wgsl # ggml/src/ggml-zendnn/CMakeLists.txt # ggml/src/ggml-zendnn/ggml-zendnn.cpp # scripts/snapdragon/adb/run-completion.sh # tests/CMakeLists.txt # tools/cli/README.md # tools/completion/README.md # tools/mtmd/clip-impl.h # tools/mtmd/clip.cpp # tools/mtmd/clip.h # tools/server/README.md
This commit is contained in:
+1
-1
@@ -179,7 +179,7 @@
|
||||
isEditing = false;
|
||||
|
||||
// If canceling a new system message with placeholder content, remove it without deleting children
|
||||
if (message.role === MessageRole.SYSTEM) {
|
||||
if (message.role === MessageRole.SYSTEM && message.content === SYSTEM_MESSAGE_PLACEHOLDER) {
|
||||
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
|
||||
|
||||
if (conversationDeleted) {
|
||||
|
||||
+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}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { beforeNavigate, afterNavigate } from '$app/navigation';
|
||||
import { ChatMessage, ChatMessageUserPending } from '$lib/components/app';
|
||||
import { setChatActionsContext } from '$lib/contexts';
|
||||
import { MessageRole } from '$lib/enums';
|
||||
@@ -27,11 +29,14 @@
|
||||
interface Props {
|
||||
messages?: DatabaseMessage[];
|
||||
onUserAction?: () => void;
|
||||
onMessagesReady?: (messageCount: number) => void;
|
||||
}
|
||||
|
||||
let { messages = [], onUserAction }: Props = $props();
|
||||
let { messages = [], onUserAction, onMessagesReady }: Props = $props();
|
||||
|
||||
let allConversationMessages = $state<DatabaseMessage[]>([]);
|
||||
let isVisible = $state(false);
|
||||
let previousConversationId = $state<string | null>(null);
|
||||
|
||||
const currentConfig = config();
|
||||
|
||||
@@ -117,15 +122,51 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Single effect that tracks both conversation and message changes
|
||||
// Track conversation changes to trigger transition even on same route
|
||||
$effect(() => {
|
||||
const conversation = activeConversation();
|
||||
const currentId = conversation?.id ?? null;
|
||||
|
||||
if (conversation) {
|
||||
refreshAllMessages();
|
||||
if (currentId !== previousConversationId && previousConversationId !== null) {
|
||||
// Conversation changed - trigger fade out/in
|
||||
isVisible = false;
|
||||
requestAnimationFrame(() => {
|
||||
refreshAllMessages();
|
||||
previousConversationId = currentId;
|
||||
requestAnimationFrame(() => {
|
||||
isVisible = true;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
previousConversationId = currentId;
|
||||
if (conversation) {
|
||||
refreshAllMessages();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
void allConversationMessages;
|
||||
|
||||
onMessagesReady?.(displayMessages.length);
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isVisible = true;
|
||||
});
|
||||
});
|
||||
|
||||
beforeNavigate(() => {
|
||||
isVisible = false;
|
||||
});
|
||||
|
||||
afterNavigate(() => {
|
||||
requestAnimationFrame(() => {
|
||||
isVisible = true;
|
||||
});
|
||||
});
|
||||
|
||||
let displayMessages = $derived.by(() => {
|
||||
if (!messages.length) {
|
||||
return [];
|
||||
@@ -207,42 +248,47 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#each displayMessages as { message, toolMessages, isLastAssistantMessage, siblingInfo } (message.id)}
|
||||
<ChatMessage
|
||||
class="mx-auto mt-12 w-full max-w-[48rem]"
|
||||
{message}
|
||||
{toolMessages}
|
||||
{isLastAssistantMessage}
|
||||
{siblingInfo}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if activeConversation() && agenticPendingSteeringMessageContent(activeConversation()!.id)}
|
||||
{@const convId = activeConversation()!.id}
|
||||
{@const pendingContent = agenticPendingSteeringMessageContent(convId)}
|
||||
|
||||
{#if pendingContent}
|
||||
<ChatMessageUserPending
|
||||
<div
|
||||
class="transition-opacity delay-300 duration-500 ease-out
|
||||
{isVisible ? 'opacity-100' : 'opacity-0'}"
|
||||
>
|
||||
{#each displayMessages as { message, toolMessages, isLastAssistantMessage, siblingInfo } (message.id)}
|
||||
<ChatMessage
|
||||
class="mx-auto mt-12 w-full max-w-[48rem]"
|
||||
content={pendingContent}
|
||||
extras={agenticPendingSteeringMessageExtras(convId)}
|
||||
onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
|
||||
onEdit={(newContent, extras) => agenticInjectSteeringMessage(convId, newContent, extras)}
|
||||
onDelete={() => agenticClearSteeringMessage(convId)}
|
||||
{message}
|
||||
{toolMessages}
|
||||
{isLastAssistantMessage}
|
||||
{siblingInfo}
|
||||
/>
|
||||
{/if}
|
||||
{:else if activeConversation() && chatPendingMessageContent(activeConversation()!.id)}
|
||||
{@const convId = activeConversation()!.id}
|
||||
{@const pendingContent = chatPendingMessageContent(convId)}
|
||||
{/each}
|
||||
|
||||
{#if pendingContent}
|
||||
<ChatMessageUserPending
|
||||
class="mx-auto mt-12 w-full max-w-[48rem]"
|
||||
content={pendingContent}
|
||||
extras={chatPendingMessageExtras(convId)}
|
||||
onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
|
||||
onEdit={(newContent, extras) => chatInjectPendingMessage(convId, newContent, extras)}
|
||||
onDelete={() => chatClearPendingMessage(convId)}
|
||||
/>
|
||||
{#if activeConversation() && agenticPendingSteeringMessageContent(activeConversation()!.id)}
|
||||
{@const convId = activeConversation()!.id}
|
||||
{@const pendingContent = agenticPendingSteeringMessageContent(convId)}
|
||||
|
||||
{#if pendingContent}
|
||||
<ChatMessageUserPending
|
||||
class="mx-auto mt-12 w-full max-w-[48rem]"
|
||||
content={pendingContent}
|
||||
extras={agenticPendingSteeringMessageExtras(convId)}
|
||||
onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
|
||||
onEdit={(newContent, extras) => agenticInjectSteeringMessage(convId, newContent, extras)}
|
||||
onDelete={() => agenticClearSteeringMessage(convId)}
|
||||
/>
|
||||
{/if}
|
||||
{:else if activeConversation() && chatPendingMessageContent(activeConversation()!.id)}
|
||||
{@const convId = activeConversation()!.id}
|
||||
{@const pendingContent = chatPendingMessageContent(convId)}
|
||||
|
||||
{#if pendingContent}
|
||||
<ChatMessageUserPending
|
||||
class="mx-auto mt-12 w-full max-w-[48rem]"
|
||||
content={pendingContent}
|
||||
extras={chatPendingMessageExtras(convId)}
|
||||
onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
|
||||
onEdit={(newContent, extras) => chatInjectPendingMessage(convId, newContent, extras)}
|
||||
onDelete={() => chatClearPendingMessage(convId)}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
|
||||
let { showCenteredEmpty = false } = $props();
|
||||
|
||||
const autoScroll = createAutoScrollController();
|
||||
|
||||
let disableAutoScroll = $derived(Boolean(config().disableAutoScroll));
|
||||
let chatScrollContainer: HTMLDivElement | undefined = $state();
|
||||
let dragCounter = $state(0);
|
||||
@@ -49,8 +51,6 @@
|
||||
let showFileErrorDialog = $state(false);
|
||||
let uploadedFiles = $state<ChatUploadedFile[]>([]);
|
||||
|
||||
const autoScroll = createAutoScrollController({ isColumnReverse: true });
|
||||
|
||||
let fileErrorData = $state<{
|
||||
generallyUnsupported: File[];
|
||||
modalityUnsupported: File[];
|
||||
@@ -322,6 +322,14 @@
|
||||
}
|
||||
});
|
||||
|
||||
function handleMessagesReady() {
|
||||
if (!disableAutoScroll && !autoScroll.userScrolledUp) {
|
||||
requestAnimationFrame(() => {
|
||||
autoScroll.scrollToBottom('instant');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
autoScroll.startObserving();
|
||||
|
||||
@@ -357,7 +365,7 @@
|
||||
<div
|
||||
bind:this={chatScrollContainer}
|
||||
aria-label="Chat interface with file drop zone"
|
||||
class="flex h-full flex-col-reverse overflow-y-auto px-4 md:px-6"
|
||||
class="flex h-full flex-col overflow-y-auto px-4 md:px-6"
|
||||
ondragenter={handleDragEnter}
|
||||
ondragleave={handleDragLeave}
|
||||
ondragover={handleDragOver}
|
||||
@@ -371,8 +379,11 @@
|
||||
messages={activeMessages()}
|
||||
onUserAction={() => {
|
||||
autoScroll.enable();
|
||||
autoScroll.scrollToBottom();
|
||||
if (!autoScroll.userScrolledUp) {
|
||||
autoScroll.scrollToBottom();
|
||||
}
|
||||
}}
|
||||
onMessagesReady={handleMessagesReady}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -31,9 +31,12 @@
|
||||
|
||||
let parsed = $derived(ModelsService.parseModelId(modelId));
|
||||
let resolvedShowRaw = $derived(showRaw ?? (config().showRawModelNames as boolean) ?? false);
|
||||
let displayName = $derived(parsed.modelName ?? modelId);
|
||||
let allAliases = $derived(aliases ?? []);
|
||||
let allTags = $derived([...(parsed.tags ?? []), ...(tags ?? [])]);
|
||||
|
||||
let uniqueAliases = $derived([...new Set(aliases ?? [])]);
|
||||
let uniqueTags = $derived([...new Set([...(parsed.tags ?? []), ...(tags ?? [])])]);
|
||||
|
||||
let primaryAlias = $derived(uniqueAliases.length === 1 ? uniqueAliases[0] : null);
|
||||
let displayName = $derived(primaryAlias ?? parsed.modelName ?? modelId);
|
||||
</script>
|
||||
|
||||
{#if resolvedShowRaw}
|
||||
@@ -56,14 +59,18 @@
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if allAliases.length > 0}
|
||||
{#each allAliases as alias (alias)}
|
||||
{#if primaryAlias}
|
||||
{#if primaryAlias !== parsed.modelName}
|
||||
<span class={badgeClass}>{parsed.modelName ?? modelId}</span>
|
||||
{/if}
|
||||
{:else if uniqueAliases.length > 1}
|
||||
{#each uniqueAliases as alias (alias)}
|
||||
<span class={badgeClass}>{alias}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if allTags.length > 0}
|
||||
{#each allTags as tag (tag)}
|
||||
{#if uniqueTags.length > 0}
|
||||
{#each uniqueTags as tag (tag)}
|
||||
<span class={tagBadgeClass}>{tag}</span>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { AUTO_SCROLL_AT_BOTTOM_THRESHOLD, AUTO_SCROLL_INTERVAL } from '$lib/cons
|
||||
|
||||
export interface AutoScrollOptions {
|
||||
disabled?: boolean;
|
||||
isColumnReverse?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -12,24 +11,19 @@ export interface AutoScrollOptions {
|
||||
* - Auto-scrolls to bottom during streaming/loading
|
||||
* - Stops auto-scroll when user manually scrolls up
|
||||
* - Resumes auto-scroll when user scrolls back to bottom
|
||||
* - Supports both normal and column-reverse scroll containers
|
||||
*/
|
||||
export class AutoScrollController {
|
||||
private _autoScrollEnabled = $state(true);
|
||||
private _userScrolledUp = $state(false);
|
||||
private _lastScrollTop = $state(0);
|
||||
private _scrollInterval: ReturnType<typeof setInterval> | undefined;
|
||||
private _scrollTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
private _container: HTMLElement | undefined;
|
||||
private _disabled: boolean;
|
||||
private _isColumnReverse: boolean;
|
||||
private _mutationObserver: MutationObserver | null = null;
|
||||
private _rafPending = false;
|
||||
private _observerEnabled = false;
|
||||
|
||||
constructor(options: AutoScrollOptions = {}) {
|
||||
this._disabled = options.disabled ?? false;
|
||||
this._isColumnReverse = options.isColumnReverse ?? false;
|
||||
}
|
||||
|
||||
get autoScrollEnabled(): boolean {
|
||||
@@ -56,6 +50,7 @@ export class AutoScrollController {
|
||||
* Updates the disabled state.
|
||||
*/
|
||||
setDisabled(disabled: boolean): void {
|
||||
if (this._disabled === disabled) return;
|
||||
this._disabled = disabled;
|
||||
if (disabled) {
|
||||
this._autoScrollEnabled = false;
|
||||
@@ -73,20 +68,8 @@ export class AutoScrollController {
|
||||
if (this._disabled || !this._container) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = this._container;
|
||||
|
||||
let distanceFromBottom: number;
|
||||
let isScrollingUp: boolean;
|
||||
|
||||
if (this._isColumnReverse) {
|
||||
// column-reverse: scrollTop=0 at bottom, negative when scrolled up
|
||||
distanceFromBottom = Math.abs(scrollTop);
|
||||
isScrollingUp = scrollTop < this._lastScrollTop;
|
||||
} else {
|
||||
// normal: scrollTop=0 at top, increases when scrolled down
|
||||
distanceFromBottom = scrollHeight - clientHeight - scrollTop;
|
||||
isScrollingUp = scrollTop < this._lastScrollTop;
|
||||
}
|
||||
|
||||
const distanceFromBottom = scrollHeight - clientHeight - scrollTop;
|
||||
const isScrollingUp = scrollTop < this._lastScrollTop;
|
||||
const isAtBottom = distanceFromBottom < AUTO_SCROLL_AT_BOTTOM_THRESHOLD;
|
||||
|
||||
if (isScrollingUp && !isAtBottom) {
|
||||
@@ -97,17 +80,6 @@ export class AutoScrollController {
|
||||
this._autoScrollEnabled = true;
|
||||
}
|
||||
|
||||
if (this._scrollTimeout) {
|
||||
clearTimeout(this._scrollTimeout);
|
||||
}
|
||||
|
||||
this._scrollTimeout = setTimeout(() => {
|
||||
if (isAtBottom) {
|
||||
this._userScrolledUp = false;
|
||||
this._autoScrollEnabled = true;
|
||||
}
|
||||
}, AUTO_SCROLL_INTERVAL);
|
||||
|
||||
this._lastScrollTop = scrollTop;
|
||||
}
|
||||
|
||||
@@ -116,13 +88,7 @@ export class AutoScrollController {
|
||||
*/
|
||||
scrollToBottom(behavior: ScrollBehavior = 'smooth'): void {
|
||||
if (this._disabled || !this._container) return;
|
||||
|
||||
if (this._isColumnReverse) {
|
||||
// column-reverse: scrollTop=0 is the bottom
|
||||
this._container.scrollTo({ top: 0, behavior });
|
||||
} else {
|
||||
this._container.scrollTo({ top: this._container.scrollHeight, behavior });
|
||||
}
|
||||
this._container.scrollTo({ top: this._container.scrollHeight, behavior });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,11 +146,6 @@ export class AutoScrollController {
|
||||
destroy(): void {
|
||||
this.stopInterval();
|
||||
this._doStopObserving();
|
||||
|
||||
if (this._scrollTimeout) {
|
||||
clearTimeout(this._scrollTimeout);
|
||||
this._scrollTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,20 +171,13 @@ export class AutoScrollController {
|
||||
private _doStartObserving(): void {
|
||||
if (!this._container || this._mutationObserver) return;
|
||||
|
||||
const isReverse = this._isColumnReverse;
|
||||
|
||||
this._mutationObserver = new MutationObserver(() => {
|
||||
if (!this._autoScrollEnabled || this._rafPending) return;
|
||||
this._rafPending = true;
|
||||
requestAnimationFrame(() => {
|
||||
this._rafPending = false;
|
||||
if (this._autoScrollEnabled && this._container) {
|
||||
if (isReverse) {
|
||||
// column-reverse: scrollTop=0 is the bottom
|
||||
this._container.scrollTop = 0;
|
||||
} else {
|
||||
this._container.scrollTop = this._container.scrollHeight;
|
||||
}
|
||||
this._container.scrollTop = this._container.scrollHeight;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,7 +130,8 @@ export class ChatService {
|
||||
timings_per_token,
|
||||
// Config options
|
||||
disableReasoningParsing,
|
||||
excludeReasoningFromContext
|
||||
excludeReasoningFromContext,
|
||||
continueFinalMessage
|
||||
} = options;
|
||||
|
||||
const normalizedMessages: ApiChatMessageData[] = messages
|
||||
@@ -209,6 +210,11 @@ export class ChatService {
|
||||
? ReasoningFormat.NONE
|
||||
: ReasoningFormat.AUTO;
|
||||
|
||||
if (continueFinalMessage) {
|
||||
requestBody.continue_final_message = true;
|
||||
requestBody.add_generation_prompt = false;
|
||||
}
|
||||
|
||||
if (temperature !== undefined) requestBody.temperature = temperature;
|
||||
if (max_tokens !== undefined) {
|
||||
// Set max_tokens to -1 (infinite) when explicitly configured as 0 or null
|
||||
|
||||
@@ -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 = '';
|
||||
@@ -1283,6 +1301,7 @@ class ChatStore {
|
||||
contextWithContinue,
|
||||
{
|
||||
...this.getApiOptions(),
|
||||
continueFinalMessage: true,
|
||||
onChunk: (chunk: string) => {
|
||||
appendedContent += chunk;
|
||||
hasReceivedContent = true;
|
||||
@@ -1291,6 +1310,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
|
||||
});
|
||||
|
||||
+3
@@ -239,6 +239,9 @@ export interface ApiChatCompletionRequest {
|
||||
// Custom parameters (JSON string)
|
||||
custom?: Record<string, unknown>;
|
||||
timings_per_token?: boolean;
|
||||
// Continuation control (vLLM compat)
|
||||
add_generation_prompt?: boolean;
|
||||
continue_final_message?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiChatCompletionToolCallFunctionDelta {
|
||||
|
||||
@@ -92,6 +92,8 @@ export interface SettingsChatServiceOptions {
|
||||
// Custom parameters
|
||||
custom?: string;
|
||||
timings_per_token?: boolean;
|
||||
// Continuation control (vLLM compat), opt in to the explicit continue final message flag
|
||||
continueFinalMessage?: boolean;
|
||||
// Callbacks
|
||||
onChunk?: (chunk: string) => void;
|
||||
onReasoningChunk?: (chunk: string) => void;
|
||||
|
||||
Reference in New Issue
Block a user