Skip to content

Commit 7cb8576

Browse files
ui: fix stop and reasoning skip in single-model mode (ggml-org#25084)
1 parent fa72bc6 commit 7cb8576

4 files changed

Lines changed: 41 additions & 9 deletions

File tree

tools/server/server-context.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2450,6 +2450,8 @@ struct server_context_impl {
24502450

24512451
server_slot * slot = get_slot_by_cmpl_id(task.params.control_cmpl_id);
24522452
if (slot == nullptr) {
2453+
SRV_WRN("control %s on unknown completion id=%s, no live slot\n",
2454+
task.params.control_action.c_str(), task.params.control_cmpl_id.c_str());
24532455
res->success = false;
24542456
res->message = "no active completion for this id";
24552457
queue_results.send(std::move(res));

tools/server/server-models.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1983,7 +1983,10 @@ void server_models_routes::init_routes() {
19831983
cli.set_read_timeout(0, STREAM_LOOKUP_TIMEOUT_MS * 1000);
19841984
cli.set_write_timeout(0, STREAM_LOOKUP_TIMEOUT_MS * 1000);
19851985
auto resp = cli.Delete(child_path.c_str());
1986-
(void) resp; // best effort, 404 and network errors are equivalent to no op
1986+
(void) resp; // the child logs its own miss when the session is unknown there
1987+
} else {
1988+
SRV_WRN("router stop for unknown conv_id=%s, no owning child in the conv map\n",
1989+
conv_id.c_str());
19871990
}
19881991
// drop the tracking entry, the session is being torn down
19891992
models.conv_models.forget(conv_id);

tools/server/server-stream.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,13 @@ void stream_session_manager::evict_and_cancel(const std::string & conversation_i
218218
std::unique_lock<std::shared_mutex> lock(map_mu);
219219
auto it = sessions.find(conversation_id);
220220
if (it == sessions.end()) {
221+
std::string live;
222+
for (const auto & kv : sessions) {
223+
if (!live.empty()) live += ", ";
224+
live += kv.first;
225+
}
226+
SRV_WRN("stop on unknown stream session, conv_id=%s matched nothing, %zu live: [%s]\n",
227+
conversation_id.c_str(), sessions.size(), live.c_str());
221228
return;
222229
}
223230
s = it->second;

tools/ui/src/lib/stores/chat.svelte.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,13 @@ class ChatStore {
154154
});
155155
if (convId === conversationsStore.activeConversation?.id) this.currentResponse = response;
156156
}
157-
private clearChatStreaming(convId: string): void {
157+
private clearChatStreaming(convId: string, messageId?: string): void {
158+
// session aware: a stale generation must not wipe a newer one's streaming state on the
159+
// same conversation, that would drop the frozen stop identity and stop the wrong session
160+
if (messageId !== undefined) {
161+
const cur = this.chatStreamingStates.get(convId);
162+
if (cur && cur.messageId !== messageId) return;
163+
}
158164
this.chatStreamingStates.delete(convId);
159165
if (convId === conversationsStore.activeConversation?.id) this.currentResponse = '';
160166
}
@@ -1055,11 +1061,14 @@ class ChatStore {
10551061
modelOverride?: string | null,
10561062
firstUserMessageContent?: string
10571063
): Promise<void> {
1058-
let effectiveModel = modelOverride;
1064+
// the ::model suffix in the stream identity is only for router mode, where it routes to the
1065+
// owning child. in single-model mode the identity stays the bare conv id so that attach, stop
1066+
// and reattach all agree, regardless of fresh send vs regenerate passing a resolved model
1067+
let effectiveModel: string | null | undefined = undefined;
10591068

1060-
if (isRouterMode() && !effectiveModel) {
1069+
if (isRouterMode()) {
10611070
const conversationModel = this.getConversationModel(allMessages);
1062-
effectiveModel = selectedModelName() || conversationModel;
1071+
effectiveModel = modelOverride || selectedModelName() || conversationModel;
10631072
}
10641073

10651074
if (isRouterMode() && effectiveModel) {
@@ -1074,6 +1083,9 @@ class ChatStore {
10741083
let resolvedModel: string | null = null;
10751084
let modelPersisted = false;
10761085
const convId = assistantMessage.convId;
1086+
// freeze the POST identity from t0 so a stop cancels with the exact session key,
1087+
// never a stale or empty model resolved later
1088+
this.setChatStreaming(convId, streamedContent, currentMessageId, effectiveModel);
10771089

10781090
const recordModel = (modelName: string | null | undefined, persistImmediately = true): void => {
10791091
if (!modelName) return;
@@ -1103,15 +1115,15 @@ class ChatStore {
11031115
};
11041116

11051117
const updateStreamingUI = () => {
1106-
this.setChatStreaming(convId, streamedContent, currentMessageId);
1118+
this.setChatStreaming(convId, streamedContent, currentMessageId, effectiveModel);
11071119
const idx = conversationsStore.findMessageIndex(currentMessageId);
11081120
conversationsStore.updateMessageAtIndex(idx, { content: streamedContent });
11091121
};
11101122

11111123
const cleanupStreamingState = () => {
11121124
this.setStreamingActive(false);
11131125
this.setChatLoading(convId, false);
1114-
this.clearChatStreaming(convId);
1126+
this.clearChatStreaming(convId, currentMessageId);
11151127
this.setProcessingState(convId, null);
11161128
};
11171129

@@ -1128,7 +1140,7 @@ class ChatStore {
11281140
onReasoningChunk: (chunk: string) => {
11291141
streamedReasoningContent += chunk;
11301142
// mark streaming state so a stop mid-thinking can persist the partial reasoning
1131-
this.setChatStreaming(convId, streamedContent, currentMessageId);
1143+
this.setChatStreaming(convId, streamedContent, currentMessageId, effectiveModel);
11321144
const idx = conversationsStore.findMessageIndex(currentMessageId);
11331145
conversationsStore.updateMessageAtIndex(idx, {
11341146
reasoningContent: streamedReasoningContent
@@ -1405,7 +1417,7 @@ class ChatStore {
14051417
// detached drain keeps producing tokens until eos or max_tokens. use the frozen identity
14061418
// captured when the session started, not the live dropdown
14071419
const streamStateForStop = this.chatStreamingStates.get(convId);
1408-
const modelForStop = streamStateForStop?.model ?? selectedModelName();
1420+
const modelForStop = streamStateForStop?.model;
14091421
void ChatService.cancelServerStream(convId, modelForStop);
14101422
this.abortRequest(convId);
14111423
this.setChatLoading(convId, false);
@@ -1846,6 +1858,14 @@ class ChatStore {
18461858
updateStreamingContent(originalContent + appendedContent);
18471859
this.setChatReasoning(msg.convId, false);
18481860
},
1861+
onCompletionId: (id: string) => {
1862+
if (!id) return;
1863+
// refresh the message id so a later skip targets the live slot after a continue
1864+
conversationsStore.updateMessageAtIndex(conversationsStore.findMessageIndex(msg.id), {
1865+
completionId: id
1866+
});
1867+
DatabaseService.updateMessage(msg.id, { completionId: id }).catch(() => {});
1868+
},
18491869
onReasoningChunk: (chunk: string) => {
18501870
appendedReasoning += chunk;
18511871
hasReceivedContent = true;

0 commit comments

Comments
 (0)