diff --git a/packages/openai-adapters/src/apis/openaiResponses.ts b/packages/openai-adapters/src/apis/openaiResponses.ts index 7ba62228451..1d060fad9bb 100644 --- a/packages/openai-adapters/src/apis/openaiResponses.ts +++ b/packages/openai-adapters/src/apis/openaiResponses.ts @@ -26,7 +26,6 @@ import { ResponseInputImage, ResponseInputText, ResponseOutputItem, - ResponseOutputMessage, ResponseOutputRefusal, ResponseOutputText, ResponseReasoningSummaryTextDeltaEvent, @@ -288,7 +287,6 @@ export function toResponsesInput( messages: ChatCompletionMessageParam[], ): ResponseInput { const inputItems: ResponseInput = []; - let assistantMessageCounter = 0; for (const message of messages) { if (message.role === "tool") { @@ -337,18 +335,19 @@ export function toResponsesInput( assistantMessage.refusal ?? null, ); if (assistantContentParts.length > 0) { - const providedId = (message as any).id; - const assistantId = - typeof providedId === "string" && providedId.startsWith("msg_") - ? providedId - : `msg_${(assistantMessageCounter++).toString().padStart(4, "0")}`; + // Chat Completions history does not carry the Responses reasoning item + // associated with an output message. Replay it as an easy input message + // without inventing a msg_ ID that would require that missing reasoning. + const content = assistantContentParts + .map((part) => + part.type === "output_text" ? part.text : part.refusal, + ) + .join(""); inputItems.push({ type: "message", role: "assistant", - content: assistantContentParts, - id: assistantId, - status: "completed", - } as ResponseOutputMessage as any); + content, + }); } if (assistantMessage.tool_calls?.length) { assistantMessage.tool_calls.forEach((toolCall, index) => { @@ -360,12 +359,9 @@ export function toResponsesInput( name: toolCall.function.name ?? "", arguments: toolCall.function.arguments ?? "{}", }; - if ( - typeof toolCall.id === "string" && - toolCall.id.startsWith("fc_") - ) { - functionCall.id = toolCall.id; - } + // Chat Completions exposes only the call ID, not the Responses + // output-item ID. Reusing it as `id` can make the Responses API + // require a reasoning item that this representation cannot carry. inputItems.push(functionCall); } }); diff --git a/packages/openai-adapters/src/test/openai-responses.vitest.ts b/packages/openai-adapters/src/test/openai-responses.vitest.ts index 68173da898b..c6f77b9ef70 100644 --- a/packages/openai-adapters/src/test/openai-responses.vitest.ts +++ b/packages/openai-adapters/src/test/openai-responses.vitest.ts @@ -25,7 +25,7 @@ import { } from "../apis/openaiResponses.js"; describe("toResponsesInput", () => { - it("maps assistant text content to output_text with generated msg ids", () => { + it("does not invent Responses output item ids for assistant history", () => { const messages: ChatCompletionMessageParam[] = [ { role: "assistant", @@ -40,14 +40,9 @@ describe("toResponsesInput", () => { expect(assistant).toMatchObject({ type: "message", role: "assistant", - id: "msg_0000", }); - expect(assistant.content).toMatchObject([ - { - type: "output_text", - text: "Hello there!", - }, - ]); + expect(assistant).not.toHaveProperty("id"); + expect(assistant.content).toBe("Hello there!"); }); it("maps assistant refusal content to refusal output items", () => { @@ -63,12 +58,7 @@ describe("toResponsesInput", () => { expect(inputItems).toHaveLength(1); const assistant = inputItems[0] as any; - expect(assistant.content).toEqual([ - { - type: "refusal", - refusal: "I must decline.", - }, - ]); + expect(assistant.content).toBe("I must decline."); }); it("converts assistant structured content into output_text items", () => { @@ -82,12 +72,7 @@ describe("toResponsesInput", () => { const inputItems = toResponsesInput(messages); const assistant = inputItems[0] as any; - expect(assistant.content).toMatchObject([ - { - type: "output_text", - text: "Structured hello.", - }, - ]); + expect(assistant.content).toBe("Structured hello."); }); it("converts chat messages, multimodal content, and tool interactions into Responses input items", () => { @@ -107,7 +92,7 @@ describe("toResponsesInput", () => { role: "assistant", tool_calls: [ { - id: "fc_call_1", + id: "call_1", type: "function", function: { name: "searchDocs", @@ -146,8 +131,7 @@ describe("toResponsesInput", () => { }, { type: "function_call", - call_id: "fc_call_1", - id: "fc_call_1", + call_id: "call_1", name: "searchDocs", arguments: '{"query":"vitest expectations"}', }, @@ -157,16 +141,17 @@ describe("toResponsesInput", () => { output: "Found 3 relevant documents.", }, ]); + expect(inputItems[2]).not.toHaveProperty("id"); }); }); -it("omits function_call id when tool call id lacks fc_ prefix", () => { +it("does not treat a Chat Completions tool call id as a Responses item id", () => { const messages: ChatCompletionMessageParam[] = [ { role: "assistant", tool_calls: [ { - id: "call_custom", + id: "fc_custom", type: "function", function: { name: "lookup", @@ -184,7 +169,7 @@ it("omits function_call id when tool call id lacks fc_ prefix", () => { ) as any; expect(functionCall).toBeTruthy(); - expect(functionCall.call_id).toBe("call_custom"); + expect(functionCall.call_id).toBe("fc_custom"); expect(functionCall).not.toHaveProperty("id"); });