Skip to content

Commit 579ba40

Browse files
webui: restore OpenAI-Compatible model source of truth and unify metadata capture
This change re-establishes a single, reliable source of truth for the active model: fully aligned with the OpenAI-Compat API behavior It introduces a unified metadata flow that captures the model field from both streaming and non-streaming responses, wiring a new onModel callback through ChatService The model name is now resolved directly from the API payload rather than relying on server /props or UI assumptions ChatStore records and persists the resolved model for each assistant message during streaming, ensuring consistency across the UI and database Type definitions for API and settings were also extended to include model metadata and the onModel callback, completing the alignment with OpenAI-Compat semantics
1 parent e9d26cd commit 579ba40

File tree

4 files changed

+118
-40
lines changed

4 files changed

+118
-40
lines changed

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

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export class ChatService {
5151
onChunk,
5252
onComplete,
5353
onError,
54+
onReasoningChunk,
55+
onModel,
5456
// Generation parameters
5557
temperature,
5658
max_tokens,
@@ -196,10 +198,11 @@ export class ChatService {
196198
onChunk,
197199
onComplete,
198200
onError,
199-
options.onReasoningChunk
201+
onReasoningChunk,
202+
onModel
200203
);
201204
} else {
202-
return this.handleNonStreamResponse(response, onComplete, onError);
205+
return this.handleNonStreamResponse(response, onComplete, onError, onModel);
203206
}
204207
} catch (error) {
205208
if (error instanceof Error && error.name === 'AbortError') {
@@ -257,7 +260,8 @@ export class ChatService {
257260
timings?: ChatMessageTimings
258261
) => void,
259262
onError?: (error: Error) => void,
260-
onReasoningChunk?: (chunk: string) => void
263+
onReasoningChunk?: (chunk: string) => void,
264+
onModel?: (model: string) => void
261265
): Promise<void> {
262266
const reader = response.body?.getReader();
263267

@@ -271,6 +275,7 @@ export class ChatService {
271275
let hasReceivedData = false;
272276
let lastTimings: ChatMessageTimings | undefined;
273277
let streamFinished = false;
278+
let modelEmitted = false;
274279

275280
try {
276281
let chunk = '';
@@ -280,7 +285,7 @@ export class ChatService {
280285

281286
chunk += decoder.decode(value, { stream: true });
282287
const lines = chunk.split('\n');
283-
chunk = lines.pop() || ''; // Save incomplete line for next read
288+
chunk = lines.pop() || '';
284289

285290
for (const line of lines) {
286291
if (line.startsWith('data: ')) {
@@ -293,6 +298,12 @@ export class ChatService {
293298
try {
294299
const parsed: ApiChatCompletionStreamChunk = JSON.parse(data);
295300

301+
const chunkModel = this.extractModelName(parsed);
302+
if (chunkModel && !modelEmitted) {
303+
modelEmitted = true;
304+
onModel?.(chunkModel);
305+
}
306+
296307
const content = parsed.choices[0]?.delta?.content;
297308
const reasoningContent = parsed.choices[0]?.delta?.reasoning_content;
298309
const timings = parsed.timings;
@@ -301,7 +312,6 @@ export class ChatService {
301312
if (timings || promptProgress) {
302313
this.updateProcessingState(timings, promptProgress);
303314

304-
// Store the latest timing data
305315
if (timings) {
306316
lastTimings = timings;
307317
}
@@ -361,7 +371,8 @@ export class ChatService {
361371
reasoningContent?: string,
362372
timings?: ChatMessageTimings
363373
) => void,
364-
onError?: (error: Error) => void
374+
onError?: (error: Error) => void,
375+
onModel?: (model: string) => void
365376
): Promise<string> {
366377
try {
367378
const responseText = await response.text();
@@ -372,6 +383,11 @@ export class ChatService {
372383
}
373384

374385
const data: ApiChatCompletionResponse = JSON.parse(responseText);
386+
const responseModel = this.extractModelName(data);
387+
if (responseModel) {
388+
onModel?.(responseModel);
389+
}
390+
375391
const content = data.choices[0]?.message?.content || '';
376392
const reasoningContent = data.choices[0]?.message?.reasoning_content;
377393

@@ -594,6 +610,69 @@ export class ChatService {
594610
}
595611
}
596612

613+
private extractModelName(data: unknown): string | undefined {
614+
if (!data || typeof data !== 'object') {
615+
return undefined;
616+
}
617+
618+
const record = data as Record<string, unknown>;
619+
const normalize = (value: unknown): string | undefined => {
620+
if (typeof value !== 'string') {
621+
return undefined;
622+
}
623+
624+
const trimmed = value.trim();
625+
626+
return trimmed.length > 0 ? trimmed : undefined;
627+
};
628+
629+
const rootModel = normalize(record['model']);
630+
if (rootModel) {
631+
return rootModel;
632+
}
633+
634+
const choices = record['choices'];
635+
if (!Array.isArray(choices) || choices.length === 0) {
636+
return undefined;
637+
}
638+
639+
const firstChoice = choices[0] as Record<string, unknown> | undefined;
640+
if (!firstChoice) {
641+
return undefined;
642+
}
643+
644+
const choiceModel = normalize(firstChoice['model']);
645+
if (choiceModel) {
646+
return choiceModel;
647+
}
648+
649+
const delta = firstChoice['delta'] as Record<string, unknown> | undefined;
650+
if (delta) {
651+
const deltaModel = normalize(delta['model']);
652+
if (deltaModel) {
653+
return deltaModel;
654+
}
655+
}
656+
657+
const message = firstChoice['message'] as Record<string, unknown> | undefined;
658+
if (message) {
659+
const messageModel = normalize(message['model']);
660+
if (messageModel) {
661+
return messageModel;
662+
}
663+
}
664+
665+
const metadata = firstChoice['metadata'] as Record<string, unknown> | undefined;
666+
if (metadata) {
667+
const metadataModel = normalize(metadata['model']);
668+
if (metadataModel) {
669+
return metadataModel;
670+
}
671+
}
672+
673+
return undefined;
674+
}
675+
597676
private updateProcessingState(
598677
timings?: ChatMessageTimings,
599678
promptProgress?: ChatMessagePromptProgress

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

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { DatabaseStore } from '$lib/stores/database';
22
import { chatService, slotsService } from '$lib/services';
3-
import { serverStore } from '$lib/stores/server.svelte';
43
import { config } from '$lib/stores/settings.svelte';
54
import { filterByLeafNodeId, findLeafNode, findDescendantMessages } from '$lib/utils/branching';
65
import { browser } from '$app/environment';
@@ -300,49 +299,39 @@ class ChatStore {
300299
): Promise<void> {
301300
let streamedContent = '';
302301
let streamedReasoningContent = '';
303-
let modelCaptured = false;
302+
let resolvedModel: string | null = null;
303+
let modelPersisted = false;
304304

305-
const captureModelIfNeeded = (updateDbImmediately = true): string | undefined => {
306-
if (!modelCaptured) {
307-
const currentModelName = serverStore.modelName;
305+
const recordModel = (modelName: string, persistImmediately = true): void => {
306+
const trimmedModel = modelName.trim();
308307

309-
if (currentModelName) {
310-
if (updateDbImmediately) {
311-
DatabaseStore.updateMessage(assistantMessage.id, { model: currentModelName }).catch(
312-
console.error
313-
);
314-
}
308+
if (!trimmedModel || trimmedModel === resolvedModel) {
309+
return;
310+
}
315311

316-
const messageIndex = this.findMessageIndex(assistantMessage.id);
312+
resolvedModel = trimmedModel;
317313

318-
this.updateMessageAtIndex(messageIndex, { model: currentModelName });
319-
modelCaptured = true;
314+
const messageIndex = this.findMessageIndex(assistantMessage.id);
320315

321-
return currentModelName;
322-
}
316+
this.updateMessageAtIndex(messageIndex, { model: trimmedModel });
317+
318+
if (persistImmediately && !modelPersisted) {
319+
modelPersisted = true;
320+
DatabaseStore.updateMessage(assistantMessage.id, { model: trimmedModel }).catch((error) => {
321+
console.error('Failed to persist model name:', error);
322+
modelPersisted = false;
323+
});
323324
}
324-
return undefined;
325325
};
326-
327-
let hasSyncedServerProps = false;
328-
329326
slotsService.startStreaming();
330327

331328
await chatService.sendMessage(allMessages, {
332329
...this.getApiOptions(),
333330

334331
onChunk: (chunk: string) => {
335-
if (!hasSyncedServerProps) {
336-
hasSyncedServerProps = true;
337-
void serverStore.fetchServerProps().catch((error) => {
338-
console.warn('Failed to refresh server props after first chunk:', error);
339-
});
340-
}
341-
342332
streamedContent += chunk;
343333
this.currentResponse = streamedContent;
344334

345-
captureModelIfNeeded();
346335
const messageIndex = this.findMessageIndex(assistantMessage.id);
347336
this.updateMessageAtIndex(messageIndex, {
348337
content: streamedContent
@@ -352,13 +341,15 @@ class ChatStore {
352341
onReasoningChunk: (reasoningChunk: string) => {
353342
streamedReasoningContent += reasoningChunk;
354343

355-
captureModelIfNeeded();
356-
357344
const messageIndex = this.findMessageIndex(assistantMessage.id);
358345

359346
this.updateMessageAtIndex(messageIndex, { thinking: streamedReasoningContent });
360347
},
361348

349+
onModel: (modelName: string) => {
350+
recordModel(modelName);
351+
},
352+
362353
onComplete: async (
363354
finalContent?: string,
364355
reasoningContent?: string,
@@ -377,10 +368,9 @@ class ChatStore {
377368
timings: timings
378369
};
379370

380-
const capturedModel = captureModelIfNeeded(false);
381-
382-
if (capturedModel) {
383-
updateData.model = capturedModel;
371+
if (resolvedModel && !modelPersisted) {
372+
updateData.model = resolvedModel;
373+
modelPersisted = true;
384374
}
385375

386376
await DatabaseStore.updateMessage(assistantMessage.id, updateData);

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,14 @@ export interface ApiChatCompletionRequest {
186186
}
187187

188188
export interface ApiChatCompletionStreamChunk {
189+
model?: string;
189190
choices: Array<{
191+
model?: string;
192+
metadata?: { model?: string };
190193
delta: {
191194
content?: string;
192195
reasoning_content?: string;
196+
model?: string;
193197
};
194198
}>;
195199
timings?: {
@@ -203,10 +207,14 @@ export interface ApiChatCompletionStreamChunk {
203207
}
204208

205209
export interface ApiChatCompletionResponse {
210+
model?: string;
206211
choices: Array<{
212+
model?: string;
213+
metadata?: { model?: string };
207214
message: {
208215
content: string;
209216
reasoning_content?: string;
217+
model?: string;
210218
};
211219
}>;
212220
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface SettingsChatServiceOptions {
4141
// Callbacks
4242
onChunk?: (chunk: string) => void;
4343
onReasoningChunk?: (chunk: string) => void;
44+
onModel?: (model: string) => void;
4445
onComplete?: (response: string, reasoningContent?: string, timings?: ChatMessageTimings) => void;
4546
onError?: (error: Error) => void;
4647
}

0 commit comments

Comments
 (0)