From 12a26630fc5f7ba523cf42b80ca22c9968b122c8 Mon Sep 17 00:00:00 2001 From: Synxneuos Date: Fri, 18 Sep 2026 01:45:56 +0530 Subject: [PATCH] fix(llm): restore id field in stateless reasoning replay items PR #34027 introduced a type alias OpenAIResponsesReasoningReplay = Omit and built the replay object without the id field. The Azure/OpenAI Responses API requires that reasoning items carrying encrypted_content also carry a matching id, so omitting it causes HTTP 400 invalid_payload on continuation turns. Fixes: - Remove the Omit alias so OpenAIResponsesReasoningReplay is OpenAIResponsesReasoningInput - Add id: reasoning.id to the replay object literal in lowerMessages - Make id required (Schema.String) in OpenAIResponsesReasoningItem schema - Update the join-summary test to assert id is present - Add regression test proving the id is forwarded in the lowered input Closes #49574 --- .../llm/src/protocols/openai-responses.ts | 5 ++- .../test/provider/openai-responses.test.ts | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index 4936d31c921b..076fbb0d10dc 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -55,7 +55,7 @@ const OpenAIResponsesReasoningSummaryText = Schema.Struct({ const OpenAIResponsesReasoningItem = Schema.Struct({ type: Schema.tag("reasoning"), - id: Schema.optionalKey(Schema.String), + id: Schema.String, summary: Schema.Array(OpenAIResponsesReasoningSummaryText), encrypted_content: optionalNull(Schema.String), }) @@ -103,7 +103,7 @@ type OpenAIResponsesReasoningInput = { summary: Array<{ type: "summary_text"; text: string }> encrypted_content?: string | null } -type OpenAIResponsesReasoningReplay = Omit +type OpenAIResponsesReasoningReplay = OpenAIResponsesReasoningInput const OpenAIResponsesTool = Schema.Struct({ type: Schema.tag("function"), @@ -400,6 +400,7 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ } const replay = { type: reasoning.type, + id: reasoning.id, summary: reasoning.summary, encrypted_content: reasoning.encrypted_content, } diff --git a/packages/llm/test/provider/openai-responses.test.ts b/packages/llm/test/provider/openai-responses.test.ts index cd8bad51af47..97f4764a6b17 100644 --- a/packages/llm/test/provider/openai-responses.test.ts +++ b/packages/llm/test/provider/openai-responses.test.ts @@ -1091,6 +1091,7 @@ describe("OpenAI Responses route", () => { expect(prepared.body.input).toEqual([ { type: "reasoning", + id: "rs_1", encrypted_content: "encrypted-state", summary: [ { type: "summary_text", text: "First" }, @@ -1469,4 +1470,44 @@ describe("OpenAI Responses route", () => { expect(error.message).toContain("HTTP 400") }), ) + it.effect("includes id in stateless reasoning replay item", () => + Effect.gen(function* () { + // Regression for the bug introduced by PR #34027: the replay object + // built in lowerMessages explicitly omitted id via the Omit type alias, + // causing HTTP 400 from the Responses API when encrypted_content was + // present (the API requires id and encrypted_content as a matched pair). + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_reasoning_replay_id", + model, + messages: [ + Message.assistant([ + { + type: "reasoning", + text: "I considered the question carefully.", + providerMetadata: { + openai: { + itemId: "rs_abc123", + reasoningEncryptedContent: "encrypted-abc123", + }, + }, + }, + ]), + ], + providerOptions: { openai: { store: false } }, + }), + ) + + const reasoningItem = prepared.body.input.find( + (item) => "type" in item && item.type === "reasoning", + ) + expect(reasoningItem).toBeDefined() + expect(reasoningItem).toEqual({ + type: "reasoning", + id: "rs_abc123", + encrypted_content: "encrypted-abc123", + summary: [{ type: "summary_text", text: "I considered the question carefully." }], + }) + }), + ) })