Skip to content

Commit 357fa4e

Browse files
fix: detect first valid SSE chunk and refresh server props once
1 parent aa940b5 commit 357fa4e

File tree

4 files changed

+29
-75
lines changed

4 files changed

+29
-75
lines changed

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export class ChatService {
5454
onError,
5555
onReasoningChunk,
5656
onModel,
57+
onFirstValidChunk,
5758
// Generation parameters
5859
temperature,
5960
max_tokens,
@@ -201,6 +202,7 @@ export class ChatService {
201202
onError,
202203
onReasoningChunk,
203204
onModel,
205+
onFirstValidChunk,
204206
conversationId,
205207
abortController.signal
206208
);
@@ -267,6 +269,7 @@ export class ChatService {
267269
onError?: (error: Error) => void,
268270
onReasoningChunk?: (chunk: string) => void,
269271
onModel?: (model: string) => void,
272+
onFirstValidChunk?: () => void,
270273
conversationId?: string,
271274
abortSignal?: AbortSignal
272275
): Promise<void> {
@@ -283,6 +286,7 @@ export class ChatService {
283286
let lastTimings: ChatMessageTimings | undefined;
284287
let streamFinished = false;
285288
let modelEmitted = false;
289+
let firstValidChunkEmitted = false;
286290

287291
try {
288292
let chunk = '';
@@ -311,17 +315,25 @@ export class ChatService {
311315
try {
312316
const parsed: ApiChatCompletionStreamChunk = JSON.parse(data);
313317

314-
const chunkModel = this.extractModelName(parsed);
315-
if (chunkModel && !modelEmitted) {
316-
modelEmitted = true;
317-
onModel?.(chunkModel);
318+
if (!firstValidChunkEmitted && parsed.object === 'chat.completion.chunk') {
319+
firstValidChunkEmitted = true;
320+
321+
if (!abortSignal?.aborted) {
322+
onFirstValidChunk?.();
323+
}
318324
}
319325

320326
const content = parsed.choices[0]?.delta?.content;
321327
const reasoningContent = parsed.choices[0]?.delta?.reasoning_content;
322328
const timings = parsed.timings;
323329
const promptProgress = parsed.prompt_progress;
324330

331+
const chunkModel = this.extractModelName(parsed);
332+
if (chunkModel && !modelEmitted) {
333+
modelEmitted = true;
334+
onModel?.(chunkModel);
335+
}
336+
325337
if (timings || promptProgress) {
326338
this.updateProcessingState(timings, promptProgress, conversationId);
327339
if (timings) {

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

Lines changed: 11 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -363,77 +363,31 @@ class ChatStore {
363363

364364
let resolvedModel: string | null = null;
365365
let modelPersisted = false;
366-
const PROPS_REFRESH_RETRY_DELAY_MS = 1_000;
367-
let serverPropsRefreshRequested = false;
368-
let lastPropsRefreshAttempt = 0;
369366
const currentConfig = config();
370367
const preferServerPropsModel = !currentConfig.modelSelectorEnabled;
368+
let serverPropsRefreshed = false;
369+
let updateModelFromServerProps: ((persistImmediately?: boolean) => void) | null = null;
371370

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

403-
serverPropsRefreshRequested = true;
404-
lastPropsRefreshAttempt = now;
376+
serverPropsRefreshed = true;
405377

406378
const hasExistingProps = serverStore.serverProps !== null;
407379

408380
serverStore
409381
.fetchServerProps({ silent: hasExistingProps })
410382
.then(() => {
411-
if (!resolvedModel) {
412-
return;
413-
}
414-
415-
const currentModel = serverStore.modelName;
416-
417-
if (!currentModel) {
418-
resetPropsRefreshGate({ immediate: true });
419-
return;
420-
}
421-
422-
const normalizedStoreModel = normalizeModelName(currentModel);
423-
424-
if (!normalizedStoreModel || normalizedStoreModel !== resolvedModel) {
425-
resetPropsRefreshGate({ immediate: true });
426-
}
383+
updateModelFromServerProps?.(true);
427384
})
428385
.catch((error) => {
429-
console.error('Failed to refresh server props during streaming:', error);
430-
resetPropsRefreshGate();
386+
console.warn('Failed to refresh server props after streaming started:', error);
431387
});
432388
};
433389

434390
const recordModel = (modelName: string | null | undefined, persistImmediately = true): void => {
435-
ensureServerPropsRefresh();
436-
437391
const serverModelName = serverStore.modelName;
438392
const preferredModelSource = preferServerPropsModel
439393
? (serverModelName ?? modelName ?? null)
@@ -468,9 +422,7 @@ class ChatStore {
468422
};
469423

470424
if (preferServerPropsModel) {
471-
const hasExistingProps = serverStore.serverProps !== null;
472-
473-
const updateModelFromServerProps = (persistImmediately = true) => {
425+
updateModelFromServerProps = (persistImmediately = true) => {
474426
const currentServerModel = serverStore.modelName;
475427

476428
if (!currentServerModel) {
@@ -481,15 +433,6 @@ class ChatStore {
481433
};
482434

483435
updateModelFromServerProps(false);
484-
485-
serverStore
486-
.fetchServerProps({ silent: hasExistingProps })
487-
.then(() => {
488-
updateModelFromServerProps(true);
489-
})
490-
.catch((error) => {
491-
console.warn('Failed to fetch server props before streaming:', error);
492-
});
493436
}
494437

495438
slotsService.startStreaming();
@@ -500,9 +443,10 @@ class ChatStore {
500443
{
501444
...this.getApiOptions(),
502445

446+
onFirstValidChunk: () => {
447+
refreshServerPropsOnce();
448+
},
503449
onChunk: (chunk: string) => {
504-
ensureServerPropsRefresh();
505-
506450
streamedContent += chunk;
507451
this.setConversationStreaming(
508452
assistantMessage.convId,
@@ -517,8 +461,6 @@ class ChatStore {
517461
},
518462

519463
onReasoningChunk: (reasoningChunk: string) => {
520-
ensureServerPropsRefresh();
521-
522464
streamedReasoningContent += reasoningChunk;
523465

524466
const messageIndex = this.findMessageIndex(assistantMessage.id);
@@ -535,8 +477,6 @@ class ChatStore {
535477
reasoningContent?: string,
536478
timings?: ChatMessageTimings
537479
) => {
538-
ensureServerPropsRefresh();
539-
540480
slotsService.stopStreaming();
541481

542482
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
@@ -186,6 +186,7 @@ export interface ApiChatCompletionRequest {
186186
}
187187

188188
export interface ApiChatCompletionStreamChunk {
189+
object?: string;
189190
model?: string;
190191
choices: Array<{
191192
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
@@ -42,6 +42,7 @@ export interface SettingsChatServiceOptions {
4242
onChunk?: (chunk: string) => void;
4343
onReasoningChunk?: (chunk: string) => void;
4444
onModel?: (model: string) => void;
45+
onFirstValidChunk?: () => void;
4546
onComplete?: (response: string, reasoningContent?: string, timings?: ChatMessageTimings) => void;
4647
onError?: (error: Error) => void;
4748
}

0 commit comments

Comments
 (0)