diff --git a/open-sse/translator/request/openai-responses/toResponses.ts b/open-sse/translator/request/openai-responses/toResponses.ts index 857f5dbffe6..4171bdf0e24 100644 --- a/open-sse/translator/request/openai-responses/toResponses.ts +++ b/open-sse/translator/request/openai-responses/toResponses.ts @@ -308,7 +308,7 @@ export function openaiToOpenAIResponsesRequest( if (role === "tool") { input.push({ type: "function_call_output", - call_id: clampCallId(toString(msg.tool_call_id)), + call_id: clampCallId(toString(msg.tool_call_id).trim()), output: typeof msg.content === "string" ? msg.content @@ -328,7 +328,7 @@ export function openaiToOpenAIResponsesRequest( if (role === "function") { input.push({ type: "function_call_output", - call_id: clampCallId(`call_${toString(msg.name)}`), + call_id: clampCallId(`call_${toString(msg.name).trim()}`), output: typeof msg.content === "string" ? msg.content : String(msg.content ?? ""), status: "completed", }); diff --git a/tests/unit/translator-request-openai-responses.test.ts b/tests/unit/translator-request-openai-responses.test.ts index c88507b08f5..691534a1423 100644 --- a/tests/unit/translator-request-openai-responses.test.ts +++ b/tests/unit/translator-request-openai-responses.test.ts @@ -9,9 +9,8 @@ import assert from "node:assert/strict"; // conversion in openai-responses.ts, hardened under #2893 to also catch // empty/missing call ids). These tests just pin that behavior down explicitly so a // future edit to that filter trips a red here. -const { openaiResponsesToOpenAIRequest } = await import( - "../../open-sse/translator/request/openai-responses.ts" -); +const { openaiResponsesToOpenAIRequest, openaiToOpenAIResponsesRequest } = + await import("../../open-sse/translator/request/openai-responses.ts"); type ChatMsg = { role: string; tool_call_id?: string; content?: unknown }; @@ -94,3 +93,38 @@ test("Responses -> OpenAI: mixed matched + orphan keeps only the matched output" assert.equal(toolMsgs.length, 1); assert.equal(toolMsgs[0].tool_call_id, "call_valid"); }); + +test("OpenAI -> Responses: whitespace-padded matching call ids stay paired (not orphan-dropped)", () => { + const result = openaiToOpenAIResponsesRequest( + "gpt-4o", + { + messages: [ + { role: "user", content: "read the file" }, + { + role: "assistant", + content: null, + tool_calls: [ + { id: " call_1 ", type: "function", function: { name: "read_file", arguments: "{}" } }, + ], + }, + { role: "tool", tool_call_id: " call_1 ", content: "file contents" }, + ], + }, + false, + {} + ) as { input: Array<{ type?: string; call_id?: string }> }; + + const outputs = result.input.filter((i) => i.type === "function_call_output"); + const call = result.input.find((i) => i.type === "function_call"); + assert.ok(call, "a function_call is emitted"); + assert.equal( + outputs.length, + 1, + "the tool result must survive the orphaned-output filter despite padded ids" + ); + assert.equal( + outputs[0].call_id, + call!.call_id, + "the function_call_output call_id must equal its paired function_call id" + ); +});