From 456828b365a8d71874ec23954ac0d0ae8770a762 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 26 Nov 2025 16:48:13 +0100 Subject: [PATCH] refactor: Chat requests abort handling --- tools/server/webui/src/lib/services/chat.ts | 43 +++--------------- .../webui/src/lib/stores/chat.svelte.ts | 45 +++++++++++++++++-- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/tools/server/webui/src/lib/services/chat.ts b/tools/server/webui/src/lib/services/chat.ts index 1225b3eda0..6f840f8cb7 100644 --- a/tools/server/webui/src/lib/services/chat.ts +++ b/tools/server/webui/src/lib/services/chat.ts @@ -54,11 +54,9 @@ import type { SettingsChatServiceOptions } from '$lib/types/settings'; * - Streaming response handling with real-time callbacks * - Reasoning content extraction and processing * - File attachment processing (images, PDFs, audio, text) - * - Request lifecycle management (abort, cleanup) + * - Request lifecycle management (abort via AbortSignal) */ export class ChatService { - private abortControllers: Map = new Map(); - /** * Sends a chat completion request to the llama.cpp server. * Supports both streaming and non-streaming responses with comprehensive parameter configuration. @@ -72,7 +70,8 @@ export class ChatService { async sendMessage( messages: ApiChatMessageData[] | (DatabaseMessage & { extra?: DatabaseMessageExtra[] })[], options: SettingsChatServiceOptions = {}, - conversationId?: string + conversationId?: string, + signal?: AbortSignal ): Promise { const { stream, @@ -112,15 +111,6 @@ export class ChatService { const currentConfig = config(); - const requestId = conversationId || 'default'; - - if (this.abortControllers.has(requestId)) { - this.abortControllers.get(requestId)?.abort(); - } - - const abortController = new AbortController(); - this.abortControllers.set(requestId, abortController); - const normalizedMessages: ApiChatMessageData[] = messages .map((msg) => { if ('id' in msg && 'convId' in msg && 'timestamp' in msg) { @@ -206,7 +196,7 @@ export class ChatService { method: 'POST', headers: getJsonHeaders(), body: JSON.stringify(requestBody), - signal: abortController.signal + signal }); if (!response.ok) { @@ -228,7 +218,7 @@ export class ChatService { onModel, onTimings, conversationId, - abortController.signal + signal ); return; } else { @@ -272,8 +262,6 @@ export class ChatService { onError(userFriendlyError); } throw userFriendlyError; - } finally { - this.abortControllers.delete(requestId); } } @@ -739,27 +727,6 @@ export class ChatService { } } - /** - * Aborts any ongoing chat completion request. - * Cancels the current request and cleans up the abort controller. - * - * @public - */ - public abortChatCompletionRequest(conversationId?: string): void { - if (conversationId) { - const abortController = this.abortControllers.get(conversationId); - if (abortController) { - abortController.abort(); - this.abortControllers.delete(conversationId); - } - } else { - for (const controller of this.abortControllers.values()) { - controller.abort(); - } - this.abortControllers.clear(); - } - } - /** * Injects a system message at the beginning of the conversation if configured in settings. * Checks for existing system messages to avoid duplication and retrieves the system message diff --git a/tools/server/webui/src/lib/stores/chat.svelte.ts b/tools/server/webui/src/lib/stores/chat.svelte.ts index d6d4cc4ba9..ee998ac0c1 100644 --- a/tools/server/webui/src/lib/stores/chat.svelte.ts +++ b/tools/server/webui/src/lib/stores/chat.svelte.ts @@ -61,6 +61,9 @@ class ChatStore { chatLoadingStates = new SvelteMap(); chatStreamingStates = new SvelteMap(); + // Abort controllers for per-conversation request cancellation + private abortControllers = new SvelteMap(); + // Processing state tracking - per-conversation timing/context info private processingStates = new SvelteMap(); private processingCallbacks = new SvelteSet<(state: ApiProcessingState | null) => void>(); @@ -517,6 +520,8 @@ class ChatStore { this.startStreaming(); this.setActiveProcessingConversation(assistantMessage.convId); + const abortController = this.getOrCreateAbortController(assistantMessage.convId); + await chatService.sendMessage( allMessages, { @@ -615,7 +620,8 @@ class ChatStore { if (onError) onError(error); } }, - assistantMessage.convId + assistantMessage.convId, + abortController.signal ); } @@ -673,12 +679,42 @@ class ChatStore { if (!activeConv) return; await this.savePartialResponseIfNeeded(activeConv.id); this.stopStreaming(); - chatService.abortChatCompletionRequest(activeConv.id); + this.abortRequest(activeConv.id); this.setChatLoading(activeConv.id, false); this.clearChatStreaming(activeConv.id); this.clearProcessingState(activeConv.id); } + /** + * Gets or creates an AbortController for a conversation + */ + private getOrCreateAbortController(convId: string): AbortController { + let controller = this.abortControllers.get(convId); + if (!controller || controller.signal.aborted) { + controller = new AbortController(); + this.abortControllers.set(convId, controller); + } + return controller; + } + + /** + * Aborts any ongoing request for a conversation + */ + private abortRequest(convId?: string): void { + if (convId) { + const controller = this.abortControllers.get(convId); + if (controller) { + controller.abort(); + this.abortControllers.delete(convId); + } + } else { + for (const controller of this.abortControllers.values()) { + controller.abort(); + } + this.abortControllers.clear(); + } + } + private async savePartialResponseIfNeeded(convId?: string): Promise { const conversationId = convId || conversationsStore.activeConversation?.id; if (!conversationId) return; @@ -1123,6 +1159,8 @@ class ChatStore { appendedThinking = '', hasReceivedContent = false; + const abortController = this.getOrCreateAbortController(msg.convId); + await chatService.sendMessage( contextWithContinue, { @@ -1218,7 +1256,8 @@ class ChatStore { ); } }, - msg.convId + msg.convId, + abortController.signal ); } catch (error) { if (!this.isAbortError(error)) console.error('Failed to continue message:', error);