From cd4f682c86eb59a224374dc2fbfa250270ecba8f Mon Sep 17 00:00:00 2001 From: mihneanob Date: Sat, 18 Jul 2026 13:22:03 +0300 Subject: [PATCH] skip empty reasoning in history --- packages/opencode/src/session/message-v2.ts | 3 ++ .../opencode/test/session/message-v2.test.ts | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 1bea9f52c3ec..deda63bce97b 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -360,6 +360,9 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* ( }) } if (part.type === "reasoning") { + // A provider can open and close a reasoning stream without emitting a delta. + // Replaying that empty step creates an assistant message with no usable content. + if (part.text === "" && !part.metadata) continue if (differentModel) { if (part.text.trim().length > 0) assistantMessage.parts.push({ diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 9bb688aeddb0..8563497ec56f 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -1151,6 +1151,53 @@ describe("session.message-v2.toModelMessage", () => { ]) }) + test("does not replay an empty assistant step between duplicate step-start boundaries", async () => { + const assistantID = "m-assistant" + const kimiModel: Provider.Model = { + ...model, + id: ModelV2.ID.make("kimi-k3"), + providerID: ProviderV2.ID.make("opencode-go"), + api: { + id: "kimi-k3", + url: "https://opencode.ai/zen/go/v1", + npm: "@ai-sdk/openai-compatible", + }, + capabilities: { + ...model.capabilities, + reasoning: true, + interleaved: { field: "reasoning_content" }, + }, + } + + const input: SessionV1.WithParts[] = [ + { + info: assistantInfo(assistantID, "m-parent", undefined, { + providerID: kimiModel.providerID, + modelID: kimiModel.api.id, + }), + parts: [ + { ...basePart(assistantID, "p1"), type: "step-start" }, + { ...basePart(assistantID, "p2"), type: "reasoning", text: "" }, + { ...basePart(assistantID, "p3"), type: "step-start" }, + { ...basePart(assistantID, "p4"), type: "reasoning", text: "thinking" }, + { ...basePart(assistantID, "p5"), type: "text", text: "answer" }, + ] satisfies SessionV1.Part[], + }, + ] + + expect(ProviderTransform.message(await MessageV2.toModelMessages(input, kimiModel), kimiModel, {})).toStrictEqual([ + { + role: "assistant", + content: [{ type: "text", text: "answer" }], + providerOptions: { + openaiCompatible: { + reasoning_content: "thinking", + }, + }, + }, + ]) + }) + test("drops messages that only contain step-start parts", async () => { const assistantID = "m-assistant"