From 1f0d8d97500dd42d26fc5b7066877b5b91dec21e Mon Sep 17 00:00:00 2001 From: can1357 Date: Fri, 17 Jul 2026 03:57:41 +0200 Subject: [PATCH] fix(codex): preserve Lite tool-use constraints --- .../openai-codex/request-transformer.ts | 20 ++++++++++--------- .../test/openai-codex-responses-lite.test.ts | 19 ++++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/ai/src/providers/openai-codex/request-transformer.ts b/packages/ai/src/providers/openai-codex/request-transformer.ts index 63b178b3c..d7887485f 100644 --- a/packages/ai/src/providers/openai-codex/request-transformer.ts +++ b/packages/ai/src/providers/openai-codex/request-transformer.ts @@ -279,14 +279,14 @@ export interface CodexLiteShapedBody { * `build_responses_request` with `use_responses_lite`): strips pinned image * detail, forces parallel tool calling off, moves tools into a leading * `additional_tools` developer item and the base instructions into a - * developer message, then omits top-level `instructions`/`tools` and forces - * `tool_choice: "auto"`. Because the rewrite removes top-level `tools`, any - * forced hosted-tool choice (e.g. `{ type: "web_search" }`) would leave the - * backend unable to validate the choice against a tools collection and it - * rejects the request with HTTP 400 (#5771); codex-rs always sends - * `tool_choice: "auto"` here. Shared by normal turns and both - * remote-compaction paths — codex-rs routes `/responses/compact` through the - * same builder. + * developer message, then omits top-level `instructions`/`tools`. Because the + * rewrite removes top-level `tools`, a forced hosted-tool choice (e.g. + * `{ type: "web_search" }`) would leave the backend unable to validate the + * choice against a tools collection and it rejects the request with HTTP 400 + * (#5771). Such choices must fall back to `"auto"`; explicit string constraints + * such as `"none"` and `"required"` remain valid. Shared by normal turns and + * both remote-compaction paths — codex-rs routes `/responses/compact` through + * the same builder. */ export function applyCodexResponsesLiteShape(body: CodexLiteShapedBody): void { const input = Array.isArray(body.input) ? body.input : []; @@ -303,7 +303,9 @@ export function applyCodexResponsesLiteShape(body: CodexLiteShapedBody): void { }); } body.input = [...prefix, ...input]; - body.tool_choice = "auto"; + if (body.tool_choice !== "none" && body.tool_choice !== "required") { + body.tool_choice = "auto"; + } delete body.instructions; delete body.tools; } diff --git a/packages/ai/test/openai-codex-responses-lite.test.ts b/packages/ai/test/openai-codex-responses-lite.test.ts index 69d1d8c2b..dd3face59 100644 --- a/packages/ai/test/openai-codex-responses-lite.test.ts +++ b/packages/ai/test/openai-codex-responses-lite.test.ts @@ -344,6 +344,25 @@ describe("openai-codex Responses Lite input shaping", () => { expect(noTools.parallel_tool_calls).toBe(false); }); + it("falls back from forced hosted tool choices without weakening explicit tool-use constraints", async () => { + const model = createCodexModel("gpt-5.6-terra"); + const tools = [{ type: "function", name: "handoff", parameters: { type: "object" } }]; + + const forced = await transformRequestBody( + { model: model.id, tools, tool_choice: { type: "web_search" } }, + model, + { responsesLite: true }, + ); + expect(forced.tool_choice).toBe("auto"); + expect(forced.tools).toBeUndefined(); + + const disabled = await transformRequestBody({ model: model.id, tools, tool_choice: "none" }, model, { + responsesLite: true, + }); + expect(disabled.tool_choice).toBe("none"); + expect(disabled.tools).toBeUndefined(); + }); + it("moves instructions and tools into input items under lite", async () => { const model = createCodexModel("gpt-5.6-terra"); const tools = [{ type: "function", name: "shot", parameters: { type: "object" } }];