Skip to content

Commit 91dd03b

Browse files
fix: detect first valid SSE chunk and refresh server props once
1 parent 58cb52a commit 91dd03b

File tree

4 files changed

+25
-73
lines changed

4 files changed

+25
-73
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class ChatService {
5656
onReasoningChunk,
5757
onToolCallChunk,
5858
onModel,
59+
onFirstValidChunk,
5960
// Generation parameters
6061
temperature,
6162
max_tokens,
@@ -206,6 +207,7 @@ export class ChatService {
206207
onReasoningChunk,
207208
onToolCallChunk,
208209
onModel,
210+
onFirstValidChunk,
209211
conversationId,
210212
abortController.signal
211213
);
@@ -274,6 +276,7 @@ export class ChatService {
274276
onReasoningChunk?: (chunk: string) => void,
275277
onToolCallChunk?: (chunk: string) => void,
276278
onModel?: (model: string) => void,
279+
onFirstValidChunk?: () => void,
277280
conversationId?: string,
278281
abortSignal?: AbortSignal
279282
): Promise<void> {
@@ -293,6 +296,7 @@ export class ChatService {
293296
let lastTimings: ChatMessageTimings | undefined;
294297
let streamFinished = false;
295298
let modelEmitted = false;
299+
let firstValidChunkEmitted = false;
296300

297301
const finalizeOpenToolCallBatch = () => {
298302
if (!hasOpenToolCallBatch) {
@@ -369,6 +373,14 @@ export class ChatService {
369373
onModel?.(chunkModel);
370374
}
371375

376+
if (!firstValidChunkEmitted && parsed.object === 'chat.completion.chunk') {
377+
firstValidChunkEmitted = true;
378+
379+
if (!abortSignal?.aborted) {
380+
onFirstValidChunk?.();
381+
}
382+
}
383+
372384
const content = parsed.choices[0]?.delta?.content;
373385
const reasoningContent = parsed.choices[0]?.delta?.reasoning_content;
374386
const toolCalls = parsed.choices[0]?.delta?.tool_calls;

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

Lines changed: 11 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -365,77 +365,31 @@ class ChatStore {
365365

366366
let resolvedModel: string | null = null;
367367
let modelPersisted = false;
368-
const PROPS_REFRESH_RETRY_DELAY_MS = 1_000;
369-
let serverPropsRefreshRequested = false;
370-
let lastPropsRefreshAttempt = 0;
371368
const currentConfig = config();
372369
const preferServerPropsModel = !currentConfig.modelSelectorEnabled;
370+
let serverPropsRefreshed = false;
371+
let updateModelFromServerProps: ((persistImmediately?: boolean) => void) | null = null;
373372

374-
const resetPropsRefreshGate = (options?: { immediate?: boolean }) => {
375-
serverPropsRefreshRequested = false;
376-
if (options?.immediate) {
377-
lastPropsRefreshAttempt = Date.now() - PROPS_REFRESH_RETRY_DELAY_MS;
378-
} else {
379-
lastPropsRefreshAttempt = Date.now();
380-
}
381-
};
382-
383-
const ensureServerPropsRefresh = () => {
384-
const now = Date.now();
385-
386-
if (serverPropsRefreshRequested) {
387-
if (resolvedModel) {
388-
const currentModel = serverStore.modelName;
389-
const normalizedStoreModel = currentModel ? normalizeModelName(currentModel) : null;
390-
391-
if (!normalizedStoreModel || normalizedStoreModel !== resolvedModel) {
392-
resetPropsRefreshGate({ immediate: true });
393-
} else {
394-
return;
395-
}
396-
} else {
397-
return;
398-
}
399-
}
400-
401-
if (now - lastPropsRefreshAttempt < PROPS_REFRESH_RETRY_DELAY_MS) {
373+
const refreshServerPropsOnce = () => {
374+
if (serverPropsRefreshed) {
402375
return;
403376
}
404377

405-
serverPropsRefreshRequested = true;
406-
lastPropsRefreshAttempt = now;
378+
serverPropsRefreshed = true;
407379

408380
const hasExistingProps = serverStore.serverProps !== null;
409381

410382
serverStore
411383
.fetchServerProps({ silent: hasExistingProps })
412384
.then(() => {
413-
if (!resolvedModel) {
414-
return;
415-
}
416-
417-
const currentModel = serverStore.modelName;
418-
419-
if (!currentModel) {
420-
resetPropsRefreshGate({ immediate: true });
421-
return;
422-
}
423-
424-
const normalizedStoreModel = normalizeModelName(currentModel);
425-
426-
if (!normalizedStoreModel || normalizedStoreModel !== resolvedModel) {
427-
resetPropsRefreshGate({ immediate: true });
428-
}
385+
updateModelFromServerProps?.(true);
429386
})
430387
.catch((error) => {
431-
console.error('Failed to refresh server props during streaming:', error);
432-
resetPropsRefreshGate();
388+
console.warn('Failed to refresh server props after streaming started:', error);
433389
});
434390
};
435391

436392
const recordModel = (modelName: string | null | undefined, persistImmediately = true): void => {
437-
ensureServerPropsRefresh();
438-
439393
const serverModelName = serverStore.modelName;
440394
const preferredModelSource = preferServerPropsModel
441395
? (serverModelName ?? modelName ?? null)
@@ -470,9 +424,7 @@ class ChatStore {
470424
};
471425

472426
if (preferServerPropsModel) {
473-
const hasExistingProps = serverStore.serverProps !== null;
474-
475-
const updateModelFromServerProps = (persistImmediately = true) => {
427+
updateModelFromServerProps = (persistImmediately = true) => {
476428
const currentServerModel = serverStore.modelName;
477429

478430
if (!currentServerModel) {
@@ -483,15 +435,6 @@ class ChatStore {
483435
};
484436

485437
updateModelFromServerProps(false);
486-
487-
serverStore
488-
.fetchServerProps({ silent: hasExistingProps })
489-
.then(() => {
490-
updateModelFromServerProps(true);
491-
})
492-
.catch((error) => {
493-
console.warn('Failed to fetch server props before streaming:', error);
494-
});
495438
}
496439

497440
slotsService.startStreaming();
@@ -502,9 +445,10 @@ class ChatStore {
502445
{
503446
...this.getApiOptions(),
504447

448+
onFirstValidChunk: () => {
449+
refreshServerPropsOnce();
450+
},
505451
onChunk: (chunk: string) => {
506-
ensureServerPropsRefresh();
507-
508452
streamedContent += chunk;
509453
this.setConversationStreaming(
510454
assistantMessage.convId,
@@ -519,8 +463,6 @@ class ChatStore {
519463
},
520464

521465
onReasoningChunk: (reasoningChunk: string) => {
522-
ensureServerPropsRefresh();
523-
524466
streamedReasoningContent += reasoningChunk;
525467

526468
const messageIndex = this.findMessageIndex(assistantMessage.id);
@@ -533,8 +475,6 @@ class ChatStore {
533475
},
534476

535477
onToolCallChunk: (toolCallChunk: string) => {
536-
ensureServerPropsRefresh();
537-
538478
const chunk = toolCallChunk.trim();
539479

540480
if (!chunk) {
@@ -554,8 +494,6 @@ class ChatStore {
554494
timings?: ChatMessageTimings,
555495
toolCallContent?: string
556496
) => {
557-
ensureServerPropsRefresh();
558-
559497
slotsService.stopStreaming();
560498

561499
const updateData: {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ export interface ApiChatCompletionToolCall extends ApiChatCompletionToolCallDelt
202202
}
203203

204204
export interface ApiChatCompletionStreamChunk {
205+
object?: string;
205206
model?: string;
206207
choices: Array<{
207208
model?: string;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface SettingsChatServiceOptions {
4343
onReasoningChunk?: (chunk: string) => void;
4444
onToolCallChunk?: (chunk: string) => void;
4545
onModel?: (model: string) => void;
46+
onFirstValidChunk?: () => void;
4647
onComplete?: (
4748
response: string,
4849
reasoningContent?: string,

0 commit comments

Comments
 (0)