From c41e8c31b47ed4f2aef7bd10a2b38c318f11106f Mon Sep 17 00:00:00 2001 From: MumuTW Date: Thu, 5 Mar 2026 18:37:23 +0000 Subject: [PATCH 1/5] fix(openai-adapters): map reasoning-delta to reasoning_content --- .../openai-adapters/src/test/vercelStreamConverter.test.ts | 6 +++++- packages/openai-adapters/src/vercelStreamConverter.ts | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/openai-adapters/src/test/vercelStreamConverter.test.ts b/packages/openai-adapters/src/test/vercelStreamConverter.test.ts index 839e3a8c78..ee10e6aae8 100644 --- a/packages/openai-adapters/src/test/vercelStreamConverter.test.ts +++ b/packages/openai-adapters/src/test/vercelStreamConverter.test.ts @@ -34,7 +34,11 @@ describe("convertVercelStreamPart", () => { const result = convertVercelStreamPart(part, options); expect(result).not.toBeNull(); - expect(result?.choices[0].delta.content).toBe("Let me think..."); + expect( + (result?.choices[0].delta as typeof result.choices[0].delta & { + reasoning_content?: string; + }).reasoning_content, + ).toBe("Let me think..."); }); test("returns null for tool-call (handled by tool-input-start/delta)", () => { diff --git a/packages/openai-adapters/src/vercelStreamConverter.ts b/packages/openai-adapters/src/vercelStreamConverter.ts index 53f4cc75a8..21930b5582 100644 --- a/packages/openai-adapters/src/vercelStreamConverter.ts +++ b/packages/openai-adapters/src/vercelStreamConverter.ts @@ -86,8 +86,11 @@ export function convertVercelStreamPart( }); case "reasoning-delta": - return chatChunk({ - content: part.text, + return chatChunkFromDelta({ + delta: { + // `reasoning_content` is not yet typed in OpenAI's SDK types. + reasoning_content: part.text, + } as ChatCompletionChunk.Choice["delta"], model, }); From cc093125121b5d091ae5a77f6f050f7457f23bf5 Mon Sep 17 00:00:00 2001 From: MumuTW Date: Thu, 5 Mar 2026 19:36:29 +0000 Subject: [PATCH 2/5] Fix system-message tool parser for non-terminal tool calls --- .../interceptSystemToolCalls.ts | 13 ++-- .../interceptSystemToolCalls.vitest.ts | 78 ++++++++++++++++++- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/core/tools/systemMessageTools/interceptSystemToolCalls.ts b/core/tools/systemMessageTools/interceptSystemToolCalls.ts index a89fbdcfbb..bc5d8bee3e 100644 --- a/core/tools/systemMessageTools/interceptSystemToolCalls.ts +++ b/core/tools/systemMessageTools/interceptSystemToolCalls.ts @@ -46,7 +46,7 @@ export async function* interceptSystemToolCalls( return result.value; } else { for await (const message of result.value) { - if (abortController.signal.aborted || parseState?.done) { + if (abortController.signal.aborted) { break; } // Skip non-assistant messages or messages with native tool calls @@ -96,12 +96,13 @@ export async function* interceptSystemToolCalls( }, ]; } - } else { - // Prevent content after tool calls for now - if (parseState) { - continue; + // Completed tool calls should not terminate parsing for subsequent + // chunks/messages; reset state so normal content (or another tool + // call) can be handled. + if (parseState.done) { + parseState = undefined; } - + } else { // Yield normal assistant message yield [ { diff --git a/core/tools/systemMessageTools/toolCodeblocks/interceptSystemToolCalls.vitest.ts b/core/tools/systemMessageTools/toolCodeblocks/interceptSystemToolCalls.vitest.ts index a636cc6a9d..ab49c06028 100644 --- a/core/tools/systemMessageTools/toolCodeblocks/interceptSystemToolCalls.vitest.ts +++ b/core/tools/systemMessageTools/toolCodeblocks/interceptSystemToolCalls.vitest.ts @@ -242,7 +242,7 @@ describe("interceptSystemToolCalls", () => { ).toBe("}"); }); - it("ignores content after a tool call", async () => { + it("preserves content after a tool call", async () => { const messages: ChatMessage[][] = [ [{ role: "assistant", content: "```tool\n" }], [{ role: "assistant", content: "TOOL_NAME: test_tool\n" }], @@ -250,7 +250,7 @@ describe("interceptSystemToolCalls", () => { [{ role: "assistant", content: "value1\n" }], [{ role: "assistant", content: "END_ARG\n" }], [{ role: "assistant", content: "```\n" }], - [{ role: "assistant", content: "This content should be ignored" }], + [{ role: "assistant", content: "This content should be preserved" }], ]; const generator = interceptSystemToolCalls( @@ -265,9 +265,79 @@ describe("interceptSystemToolCalls", () => { result = await generator.next(); } - // The content after the tool call should be ignored + // The content after the tool call should be preserved result = await generator.next(); - expect(result.value).toBeUndefined(); + expect(result.value).toEqual([ + { + role: "assistant", + content: [{ type: "text", text: "This content should be preserved" }], + }, + ]); + }); + + it("parses a tool call that appears mid-message and preserves trailing content", async () => { + const messages: ChatMessage[][] = [ + [ + { + role: "assistant", + content: + "Before tool\n```tool\nTOOL_NAME: test_tool\nBEGIN_ARG: arg1\nvalue1\nEND_ARG\n```\nAfter tool", + }, + ], + ]; + + const generator = interceptSystemToolCalls( + createAsyncGenerator(messages), + abortController, + framework, + ); + + let result = await generator.next(); + expect(result.value).toEqual([ + { + role: "assistant", + content: [{ type: "text", text: "Before tool" }], + }, + ]); + + result = await generator.next(); + expect(result.value).toEqual([ + { + role: "assistant", + content: [{ type: "text", text: "\n" }], + }, + ]); + + result = await generator.next(); + expect( + (result.value as AssistantChatMessage[])[0].toolCalls?.[0].function?.name, + ).toBe("test_tool"); + + result = await generator.next(); + expect( + (result.value as AssistantChatMessage[])[0].toolCalls?.[0].function + ?.arguments, + ).toContain('{"arg1":'); + + result = await generator.next(); + expect( + (result.value as AssistantChatMessage[])[0].toolCalls?.[0].function + ?.arguments, + ).toBe('"value1"'); + + result = await generator.next(); + expect( + (result.value as AssistantChatMessage[])[0].toolCalls?.[0].function + ?.arguments, + ).toBe("}"); + + result = await generator.next(); + expect(result.value).toEqual([ + { + role: "assistant", + content: [{ type: "text", text: "After tool" }], + }, + ]); }); it("stops processing when aborted", async () => { From 41780f454607bffc945b80bb3b0d5027ba77c8eb Mon Sep 17 00:00:00 2001 From: MumuTW Date: Thu, 5 Mar 2026 23:44:21 +0000 Subject: [PATCH 3/5] Fix duplicate predefined tools in dynamic system-message section --- core/tools/systemMessageTools/buildToolsSystemMessage.ts | 2 +- .../toolCodeblocks/buildSystemMessage.vitest.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/tools/systemMessageTools/buildToolsSystemMessage.ts b/core/tools/systemMessageTools/buildToolsSystemMessage.ts index 750e6bb430..5948b04456 100644 --- a/core/tools/systemMessageTools/buildToolsSystemMessage.ts +++ b/core/tools/systemMessageTools/buildToolsSystemMessage.ts @@ -40,7 +40,7 @@ export const generateToolsSystemMessage = ( `\nAlso, these additional tool definitions show other tools you can call with the same syntax:`, ); - for (const tool of tools) { + for (const tool of withDynamicMessage) { try { const definition = framework.toolToSystemToolDefinition(tool); instructions.push(`\n${definition}`); diff --git a/core/tools/systemMessageTools/toolCodeblocks/buildSystemMessage.vitest.ts b/core/tools/systemMessageTools/toolCodeblocks/buildSystemMessage.vitest.ts index 669d1c1031..4742559ef6 100644 --- a/core/tools/systemMessageTools/toolCodeblocks/buildSystemMessage.vitest.ts +++ b/core/tools/systemMessageTools/toolCodeblocks/buildSystemMessage.vitest.ts @@ -191,6 +191,11 @@ describe("generateToolsSystemMessage", () => { const hasDynamicToolsSection = /additional tool definitions/i.test(result); expect(hasDynamicToolsSection).toBe(true); + + // Dynamic definitions should not duplicate tools that already have predefined messages. + expect(result.match(/TOOL_NAME: tool_with_description/g)?.length ?? 0).toBe( + 1, + ); }); it("includes example tool definition and call", () => { From 72cfabff80844d135ea80cbd5ecb85189c315676 Mon Sep 17 00:00:00 2001 From: MumuTW Date: Fri, 6 Mar 2026 09:49:12 +0000 Subject: [PATCH 4/5] fix(openai-adapters): fix TS syntax error in vercelStreamConverter test The `as typeof X & Y` type assertion was ambiguous to the TypeScript parser without parentheses, causing TS1005 errors across all CI checks. Simplify to `as any` since this is a test assertion. Co-Authored-By: Claude Opus 4.6 --- .../src/test/vercelStreamConverter.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/openai-adapters/src/test/vercelStreamConverter.test.ts b/packages/openai-adapters/src/test/vercelStreamConverter.test.ts index ee10e6aae8..0c3feed93d 100644 --- a/packages/openai-adapters/src/test/vercelStreamConverter.test.ts +++ b/packages/openai-adapters/src/test/vercelStreamConverter.test.ts @@ -34,11 +34,10 @@ describe("convertVercelStreamPart", () => { const result = convertVercelStreamPart(part, options); expect(result).not.toBeNull(); - expect( - (result?.choices[0].delta as typeof result.choices[0].delta & { - reasoning_content?: string; - }).reasoning_content, - ).toBe("Let me think..."); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((result?.choices[0].delta as any).reasoning_content).toBe( + "Let me think...", + ); }); test("returns null for tool-call (handled by tool-input-start/delta)", () => { From ee012d3192bdd648e612420de50ad2686b734760 Mon Sep 17 00:00:00 2001 From: MumuTW Date: Tue, 10 Mar 2026 23:37:56 +0000 Subject: [PATCH 5/5] chore: remove unrelated vercelStreamConverter changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per reviewer feedback — keeping this PR focused on the system-message tools parser fix. --- .../openai-adapters/src/test/vercelStreamConverter.test.ts | 5 +---- packages/openai-adapters/src/vercelStreamConverter.ts | 7 ++----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/openai-adapters/src/test/vercelStreamConverter.test.ts b/packages/openai-adapters/src/test/vercelStreamConverter.test.ts index 0c3feed93d..839e3a8c78 100644 --- a/packages/openai-adapters/src/test/vercelStreamConverter.test.ts +++ b/packages/openai-adapters/src/test/vercelStreamConverter.test.ts @@ -34,10 +34,7 @@ describe("convertVercelStreamPart", () => { const result = convertVercelStreamPart(part, options); expect(result).not.toBeNull(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((result?.choices[0].delta as any).reasoning_content).toBe( - "Let me think...", - ); + expect(result?.choices[0].delta.content).toBe("Let me think..."); }); test("returns null for tool-call (handled by tool-input-start/delta)", () => { diff --git a/packages/openai-adapters/src/vercelStreamConverter.ts b/packages/openai-adapters/src/vercelStreamConverter.ts index 21930b5582..53f4cc75a8 100644 --- a/packages/openai-adapters/src/vercelStreamConverter.ts +++ b/packages/openai-adapters/src/vercelStreamConverter.ts @@ -86,11 +86,8 @@ export function convertVercelStreamPart( }); case "reasoning-delta": - return chatChunkFromDelta({ - delta: { - // `reasoning_content` is not yet typed in OpenAI's SDK types. - reasoning_content: part.text, - } as ChatCompletionChunk.Choice["delta"], + return chatChunk({ + content: part.text, model, });