diff --git a/changelog.d/fixes/9219-codex-additional-tools-normalization.md b/changelog.d/fixes/9219-codex-additional-tools-normalization.md new file mode 100644 index 0000000000..02ee481136 --- /dev/null +++ b/changelog.d/fixes/9219-codex-additional-tools-normalization.md @@ -0,0 +1 @@ +- **fix(codex):** normalize additional_tools passthrough items. (thanks @SalyyS1) diff --git a/open-sse/utils/responsesInputNormalization.ts b/open-sse/utils/responsesInputNormalization.ts index de1c98319f..490080c3db 100644 --- a/open-sse/utils/responsesInputNormalization.ts +++ b/open-sse/utils/responsesInputNormalization.ts @@ -77,8 +77,17 @@ function normalizeCodexResponsesInputItem(itemValue: unknown): unknown { const role = typeof item.role === "string" ? item.role : "user"; const type = typeof item.type === "string" ? item.type : ""; + if (type === "additional_tools") { + delete item.content; + return item; + } + if (!type && item.content === undefined && typeof item.text === "string") { - return { type: "message", role, content: [{ type: textPartTypeForRole(role), text: item.text }] }; + return { + type: "message", + role, + content: [{ type: textPartTypeForRole(role), text: item.text }], + }; } if (!type && role) item.type = "message"; diff --git a/tests/unit/codex-responses-passthrough-strip-3317.test.ts b/tests/unit/codex-responses-passthrough-strip-3317.test.ts index 6c64ef7703..9c755e6ff7 100644 --- a/tests/unit/codex-responses-passthrough-strip-3317.test.ts +++ b/tests/unit/codex-responses-passthrough-strip-3317.test.ts @@ -39,6 +39,32 @@ test("codex native responses passthrough strips client-only params (#3317)", asy assert.ok(Array.isArray(result.input), "input array preserved"); }); +test("codex native responses passthrough normalizes additional_tools items", async () => { + const executor = new CodexExecutor(); + const messageContent = [{ type: "input_text", text: "run the terminal tool" }]; + const tools = [{ type: "function", name: "terminal", parameters: { type: "object" } }]; + const body = { + _nativeCodexPassthrough: true, + model: "gpt-5.5", + input: [ + { + type: "additional_tools", + role: "developer", + content: [{ type: "input_text", text: "unsupported wrapper content" }], + tools, + }, + { type: "message", role: "user", content: messageContent }, + ], + }; + + const result = (await executor.transformRequest("gpt-5.5", body, true, {} as never)) as { + input: Array>; + }; + + assert.deepEqual(result.input[0], { type: "additional_tools", role: "developer", tools }); + assert.deepEqual(result.input[1], { type: "message", role: "user", content: messageContent }); +}); + test.after(() => { try { core.resetDbInstance?.(); diff --git a/tests/unit/responses-translation-fixes.test.ts b/tests/unit/responses-translation-fixes.test.ts index 0d859f7c02..1c91e64952 100644 --- a/tests/unit/responses-translation-fixes.test.ts +++ b/tests/unit/responses-translation-fixes.test.ts @@ -126,6 +126,29 @@ test("Codex Responses input: null input normalizes to an empty list (not [null]) assert.deepEqual(body.input, []); }); +test("Codex Responses input: additional_tools drops unsupported content", () => { + const body: Record = { + input: [ + { + type: "additional_tools", + role: "developer", + content: [{ type: "input_text", text: "unsupported wrapper content" }], + tools: [{ type: "function", name: "terminal", parameters: { type: "object" } }], + }, + ], + }; + + normalizeCodexResponsesInput(body); + + assert.deepEqual(body.input, [ + { + type: "additional_tools", + role: "developer", + tools: [{ type: "function", name: "terminal", parameters: { type: "object" } }], + }, + ]); +}); + test("Codex Responses input: assistant history normalized to output_text (OpenAI/Codex rejects input_text on assistant turns)", () => { const body: Record = { input: [