From d44bb41d2732b2e2065a8f83ceb371fe4a1d31d4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 19:45:55 +0000 Subject: [PATCH] fix: replay OpenAI reasoning for tool calls --- patches/@mariozechner__pi-ai@0.42.2.patch | 37 ------------------- .../openai-responses.reasoning-replay.test.ts | 7 +++- 2 files changed, 5 insertions(+), 39 deletions(-) diff --git a/patches/@mariozechner__pi-ai@0.42.2.patch b/patches/@mariozechner__pi-ai@0.42.2.patch index 32becbcbb..9814fa831 100644 --- a/patches/@mariozechner__pi-ai@0.42.2.patch +++ b/patches/@mariozechner__pi-ai@0.42.2.patch @@ -18,22 +18,6 @@ diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-co index 188a829..4555c9f 100644 --- a/dist/providers/openai-codex-responses.js +++ b/dist/providers/openai-codex-responses.js -@@ -433,9 +433,15 @@ function convertMessages(model, context) { - } - else if (msg.role === "assistant") { - const output = []; -+ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`. -+ // Tool-call-only turns (thinking + function_call) are valid assistant turns, but -+ // their stored reasoning items must not be replayed as standalone `reasoning` input. -+ const hasTextBlock = msg.content.some((b) => b.type === "text"); - for (const block of msg.content) { - if (block.type === "thinking" && msg.stopReason !== "error") { - if (block.thinkingSignature) { -+ if (!hasTextBlock) -+ continue; - const reasoningItem = JSON.parse(block.thinkingSignature); - output.push(reasoningItem); - } @@ -515,7 +521,7 @@ function convertTools(tools) { name: tool.name, description: tool.description, @@ -126,24 +110,3 @@ index 5d0813a..e0ef676 100644 stream.push({ type: "toolcall_delta", contentIndex: blockIndex(), -diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js -index f07085c..f3b01ee 100644 ---- a/dist/providers/openai-responses.js -+++ b/dist/providers/openai-responses.js -@@ -396,10 +396,16 @@ function convertMessages(model, context) { - } - else if (msg.role === "assistant") { - const output = []; -+ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`. -+ // Tool-call-only turns (thinking + function_call) are valid assistant turns, but -+ // their stored reasoning items must not be replayed as standalone `reasoning` input. -+ const hasTextBlock = msg.content.some((b) => b.type === "text"); - for (const block of msg.content) { - // Do not submit thinking blocks if the completion had an error (i.e. abort) - if (block.type === "thinking" && msg.stopReason !== "error") { - if (block.thinkingSignature) { -+ if (!hasTextBlock) -+ continue; - const reasoningItem = JSON.parse(block.thinkingSignature); - output.push(reasoningItem); - } diff --git a/src/agents/openai-responses.reasoning-replay.test.ts b/src/agents/openai-responses.reasoning-replay.test.ts index 544079330..e418327c5 100644 --- a/src/agents/openai-responses.reasoning-replay.test.ts +++ b/src/agents/openai-responses.reasoning-replay.test.ts @@ -52,7 +52,7 @@ function installFailingFetchCapture() { } describe("openai-responses reasoning replay", () => { - it("does not replay standalone reasoning for tool-call-only turns", async () => { + it("replays reasoning for tool-call-only turns", async () => { const cap = installFailingFetchCapture(); try { const model = buildModel(); @@ -142,7 +142,10 @@ describe("openai-responses reasoning replay", () => { .filter((t): t is string => typeof t === "string"); expect(types).toContain("function_call"); - expect(types).not.toContain("reasoning"); + expect(types).toContain("reasoning"); + expect(types.indexOf("reasoning")).toBeLessThan( + types.indexOf("function_call"), + ); } finally { cap.restore(); }