diff --git a/src/chat/inbound.ts b/src/chat/inbound.ts index bdf7b30551..3916d9afa1 100644 --- a/src/chat/inbound.ts +++ b/src/chat/inbound.ts @@ -310,10 +310,16 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec { // Responses assistant item schema admits only output content blocks, so there // is no attachment point on the message itself, and the parser buffers a // reasoning item and prepends it to the NEXT assistant message. Emitting it - // here keeps that adjacency intact. + // here keeps that adjacency intact. OpenAI requires `summary` on replayed + // reasoning items; keep it empty because this is raw provider reasoning, not + // a caller-authorized user-visible summary. const reasoningText = assistantReasoningText(msg); if (reasoningText !== undefined) { - input.push({ type: "reasoning", content: [{ type: "reasoning_text", text: reasoningText }] }); + input.push({ + type: "reasoning", + summary: [], + content: [{ type: "reasoning_text", text: reasoningText }], + }); } const blocks = assistantContentToBlocks(msg.content); if (blocks.length > 0) input.push({ type: "message", role: "assistant", content: blocks }); diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index aa9aa15f52..e5eea065de 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -303,7 +303,9 @@ An assistant turn's `reasoning_content` or `reasoning_details` is carried into t projection as a `reasoning` input item emitted immediately before its assistant message, matching the parser's buffer-and-prepend adjacency. Only representable plaintext crosses: no signature, encrypted payload or provider item id is -reconstructed, because those attest to content this proxy never received. Opaque +reconstructed, because those attest to content this proxy never received. The item +always carries `summary: []`, which satisfies the Responses replay shape without +publishing raw provider reasoning as a user-visible summary. Opaque reasoning replay across a Chat boundary remains unimplemented by design. `presence_penalty` and `frequency_penalty` are carried too; per-model `noPenaltyModels` opt-outs still apply at the adapter. diff --git a/tests/responses/chat-inbound-reasoning-replay.test.ts b/tests/responses/chat-inbound-reasoning-replay.test.ts index de78b3eaaa..a5044fb600 100644 --- a/tests/responses/chat-inbound-reasoning-replay.test.ts +++ b/tests/responses/chat-inbound-reasoning-replay.test.ts @@ -35,6 +35,7 @@ describe("F6 assistant reasoning survives translation", () => { const idx = out.findIndex(i => i.type === "reasoning"); expect(idx).toBeGreaterThanOrEqual(0); + expect(out[idx]!.summary).toEqual([]); expect(out[idx]!.content).toEqual([{ type: "reasoning_text", text: "prior analysis" }]); // Adjacency matters: the parser prepends a buffered reasoning item to the NEXT // assistant message, so it must sit immediately before it. @@ -57,6 +58,9 @@ describe("F6 assistant reasoning survives translation", () => { test("no signature, encrypted payload or item id is forged", () => { const item = items(body([USER, { role: "assistant", content: "a", reasoning_content: "t" }])).find(i => i.type === "reasoning")!; + // Raw provider reasoning must not be reclassified as a visible summary merely to + // satisfy the Responses wire requirement. + expect(item.summary).toEqual([]); expect(item.signature).toBeUndefined(); expect(item.encrypted_content).toBeUndefined(); expect(item.id).toBeUndefined();