fix(stream): close thinking block before text or tool output
If upstream omits reasoning-end and starts emitting text-delta or tool-call events, the thinking block remained open causing thinking_start -> thinking_delta -> text_start -> ... -> thinking_end ordering. Now endThinking() is called at the start of text-delta and tool-call handlers so thinking_end always precedes text_start or toolcall_start. Adds 2 regression tests for the missing reasoning-end edge case.
This commit is contained in:
@@ -223,6 +223,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "text-delta": {
|
case "text-delta": {
|
||||||
|
endThinking()
|
||||||
if (!textBlock) {
|
if (!textBlock) {
|
||||||
textBlock = { type: "text", text: "" }
|
textBlock = { type: "text", text: "" }
|
||||||
output.content.push(textBlock)
|
output.content.push(textBlock)
|
||||||
@@ -286,6 +287,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
|
|
||||||
case "tool-call": {
|
case "tool-call": {
|
||||||
endTextBlock()
|
endTextBlock()
|
||||||
|
endThinking()
|
||||||
const toolCall: ToolCallContent = {
|
const toolCall: ToolCallContent = {
|
||||||
type: "toolCall",
|
type: "toolCall",
|
||||||
id: stringValue(event.toolCallId) ?? "",
|
id: stringValue(event.toolCallId) ?? "",
|
||||||
|
|||||||
@@ -202,6 +202,66 @@ describe("streamCommandCode — successful streams", () => {
|
|||||||
if (done?.type !== "done") throw new Error("expected done")
|
if (done?.type !== "done") throw new Error("expected done")
|
||||||
assert.equal(done.message.content[0]?.type, "thinking")
|
assert.equal(done.message.content[0]?.type, "thinking")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("closes thinking block before text when reasoning-end is missing", async () => {
|
||||||
|
server.mockResponse({
|
||||||
|
type: "success",
|
||||||
|
events: [
|
||||||
|
JSON.stringify({ type: "reasoning-start" }),
|
||||||
|
JSON.stringify({ type: "reasoning-delta", text: "thinking" }),
|
||||||
|
JSON.stringify({ type: "text-delta", text: "answer" }),
|
||||||
|
JSON.stringify({ type: "finish", finishReason: "stop" }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||||
|
|
||||||
|
const events = await collectEvents(
|
||||||
|
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(eventTypes(events), [
|
||||||
|
"start",
|
||||||
|
"thinking_start",
|
||||||
|
"thinking_delta",
|
||||||
|
"thinking_end",
|
||||||
|
"text_start",
|
||||||
|
"text_delta",
|
||||||
|
"text_end",
|
||||||
|
"done",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("closes thinking block before tool-call when reasoning-end is missing", async () => {
|
||||||
|
server.mockResponse({
|
||||||
|
type: "success",
|
||||||
|
events: [
|
||||||
|
JSON.stringify({ type: "reasoning-start" }),
|
||||||
|
JSON.stringify({ type: "reasoning-delta", text: "thinking" }),
|
||||||
|
JSON.stringify({
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "call_1",
|
||||||
|
toolName: "read_file",
|
||||||
|
input: JSON.stringify({ path: "/tmp/x" }),
|
||||||
|
}),
|
||||||
|
JSON.stringify({ type: "finish", finishReason: "tool-calls" }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||||
|
|
||||||
|
const events = await collectEvents(
|
||||||
|
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(eventTypes(events), [
|
||||||
|
"start",
|
||||||
|
"thinking_start",
|
||||||
|
"thinking_delta",
|
||||||
|
"thinking_end",
|
||||||
|
"toolcall_start",
|
||||||
|
"toolcall_end",
|
||||||
|
"done",
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("streamCommandCode — request serialization", () => {
|
describe("streamCommandCode — request serialization", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user