diff --git a/changelog.d/fixes/8083-responses-input-items-status.md b/changelog.d/fixes/8083-responses-input-items-status.md new file mode 100644 index 0000000000..d184d8dbee --- /dev/null +++ b/changelog.d/fixes/8083-responses-input-items-status.md @@ -0,0 +1 @@ +- fix(translator): set `status: "completed"` on translated OpenAI Responses `input` items so strict Responses-compatible upstreams stop rejecting them with 400 MissingParameter input.status (#8083) diff --git a/open-sse/translator/request/openai-responses/toResponses.ts b/open-sse/translator/request/openai-responses/toResponses.ts index 859f2d60d7..f91b66f918 100644 --- a/open-sse/translator/request/openai-responses/toResponses.ts +++ b/open-sse/translator/request/openai-responses/toResponses.ts @@ -120,6 +120,7 @@ export function openaiToOpenAIResponsesRequest( type: "message", role: "developer", content: buildResponsesTextParts(msg.content), + status: "completed", }); continue; } @@ -185,6 +186,7 @@ export function openaiToOpenAIResponsesRequest( type: "message", role: "user", content, + status: "completed", }); } @@ -223,6 +225,7 @@ export function openaiToOpenAIResponsesRequest( type: "message", role: "assistant", content: outputContent, + status: "completed", }); } @@ -241,6 +244,7 @@ export function openaiToOpenAIResponsesRequest( call_id: clampCallId(toString(toolCall.id).trim() || generateToolCallId()), name: fnName, arguments: toString(fn.arguments, "{}"), + status: "completed", }); } } @@ -255,6 +259,7 @@ export function openaiToOpenAIResponsesRequest( call_id: clampCallId(`call_${fnName}`), name: fnName, arguments: toString(fc.arguments, "{}"), + status: "completed", }); } } @@ -276,6 +281,7 @@ export function openaiToOpenAIResponsesRequest( return c; }) : String(msg.content ?? ""), + status: "completed", }); } @@ -285,6 +291,7 @@ export function openaiToOpenAIResponsesRequest( type: "function_call_output", call_id: clampCallId(`call_${toString(msg.name)}`), output: typeof msg.content === "string" ? msg.content : String(msg.content ?? ""), + status: "completed", }); } } diff --git a/tests/unit/responses-input-item-status-8083.test.ts b/tests/unit/responses-input-item-status-8083.test.ts new file mode 100644 index 0000000000..ddbedc9500 --- /dev/null +++ b/tests/unit/responses-input-item-status-8083.test.ts @@ -0,0 +1,55 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +// Kept in its own file rather than growing the frozen +// tests/unit/translator-openai-responses-req.test.ts (file-size ratchet). +const { openaiToOpenAIResponsesRequest } = await import( + "../../open-sse/translator/request/openai-responses.ts" +); + +// --- Issue #8083: strict Responses-compatible upstreams reject items whose +// `type` is set without an accompanying `status`, returning +// 400 MissingParameter input.status. `status` is optional per OpenAI's own +// schema, so setting it to "completed" on translated history items (which by +// construction always represent an already-completed prior turn) is a safe +// superset fix that also satisfies stricter third-party validators. +test("Chat -> Responses sets status:completed on every input item (#8083)", () => { + const result = openaiToOpenAIResponsesRequest( + "gpt-4o", + { + messages: [ + { role: "system", content: "Rules" }, + // Mid-conversation developer turn -> developer-role message item. + { role: "developer", content: "Follow up instruction" }, + { role: "user", content: "Hello" }, + { + role: "assistant", + content: "Done", + tool_calls: [ + { id: "call_1", type: "function", function: { name: "read_file", arguments: "{}" } }, + ], + }, + { role: "tool", tool_call_id: "call_1", content: "ok" }, + // Deprecated function_call / function role pair. + { + role: "assistant", + function_call: { name: "legacy_fn", arguments: "{}" }, + }, + { role: "function", name: "legacy_fn", content: "legacy ok" }, + ], + }, + false, + null + ) as Record; + + const input = result.input as Array>; + assert.ok(input.length > 0, "input array must not be empty"); + for (const item of input) { + assert.equal( + item.status, + "completed", + `input item of type "${item.type}" is missing status:"completed" — strict Responses-compatible ` + + `upstreams reject this with 400 MissingParameter input.status (#8083)` + ); + } +}); diff --git a/tests/unit/translator-openai-responses-req.test.ts b/tests/unit/translator-openai-responses-req.test.ts index 9618961bcc..b12038606b 100644 --- a/tests/unit/translator-openai-responses-req.test.ts +++ b/tests/unit/translator-openai-responses-req.test.ts @@ -360,22 +360,26 @@ test("Chat -> Responses converts messages, tool calls, tool outputs, tools and p { type: "input_image", image_url: "https://example.com/cat.png", detail: "high" }, { type: "input_file", file_data: "abc", filename: "doc.txt" }, ], + status: "completed", }, { type: "message", role: "assistant", content: [{ type: "output_text", text: "Done" }], + status: "completed", }, { type: "function_call", call_id: "call_1", name: "read_file", arguments: '{"path":"/tmp/a"}', + status: "completed", }, { type: "function_call_output", call_id: "call_1", output: [{ type: "input_text", text: "ok" }], + status: "completed", }, ]); assert.deepEqual((result as any).tools, [ @@ -520,6 +524,7 @@ test("Chat -> Responses converts assistant image_url history parts to output_tex { type: "output_text", text: "I inspected the screenshot." }, { type: "output_text", text: "[Image: https://example.com/scope.png]" }, ], + status: "completed", }, ]); assert.equal(JSON.stringify(result).includes('"image_url"'), false);