From ed68bcedf8a522e3a43c64949075cfd32cde681d Mon Sep 17 00:00:00 2001 From: Sutarto Jordan Chrisfivo Date: Sun, 21 Jun 2026 16:00:27 +0700 Subject: [PATCH] fix(codex): preserve custom tools during request normalization Add CODEX_PASSTHROUGH_TOOL_TYPES allowlist so Responses-native freeform tools (e.g. grammar-backed apply_patch) survive normalizeCodexTools and reach the upstream Codex Responses API intact. Refs #1907 Co-authored-by: Cursor --- open-sse/executors/codex.js | 4 + tests/unit/codex-tool-normalization.test.js | 99 +++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 tests/unit/codex-tool-normalization.test.js diff --git a/open-sse/executors/codex.js b/open-sse/executors/codex.js index 8c093d2a..03779b4a 100644 --- a/open-sse/executors/codex.js +++ b/open-sse/executors/codex.js @@ -26,6 +26,9 @@ const CODEX_HOSTED_TOOL_TYPES = new Set([ "tool_search" ]); +// Responses-native freeform tools carry a name plus format payload and must pass through intact. +const CODEX_PASSTHROUGH_TOOL_TYPES = new Set(["custom"]); + // Allowlist of fields accepted by Codex Responses API — anything else is stripped const RESPONSES_API_ALLOWLIST = new Set([ "model", "input", "instructions", "tools", "tool_choice", "stream", "store", @@ -72,6 +75,7 @@ function normalizeCodexTools(body) { return true; } if (type !== "function") { + if (CODEX_PASSTHROUGH_TOOL_TYPES.has(type)) return true; if (!type || tool.function || typeof tool.name === "string") return false; return CODEX_HOSTED_TOOL_TYPES.has(type); } diff --git a/tests/unit/codex-tool-normalization.test.js b/tests/unit/codex-tool-normalization.test.js new file mode 100644 index 00000000..1f3c6f94 --- /dev/null +++ b/tests/unit/codex-tool-normalization.test.js @@ -0,0 +1,99 @@ +import { describe, expect, it } from "vitest"; + +import { CodexExecutor } from "../../open-sse/executors/codex.js"; + +function normalizeTools(tools) { + const executor = new CodexExecutor(); + const body = { + model: "gpt-5.5", + input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "probe" }] }], + tools, + stream: true, + }; + + executor.transformRequest("gpt-5.5", body, true, { + connectionId: "test-codex-tools", + providerSpecificData: {}, + }); + + return body.tools; +} + +describe("CodexExecutor tool normalization", () => { + it("preserves Responses-native tool_search tools", () => { + const tools = normalizeTools([ + { + type: "tool_search", + execution: "sync", + description: "Discover deferred tools", + parameters: { type: "object", properties: {} }, + }, + { + type: "namespace", + name: "codex_app", + description: "app tools", + tools: [ + { + type: "function", + name: "automation_update", + description: "automation", + parameters: { type: "object", properties: {} }, + defer_loading: true, + }, + ], + }, + { + type: "function", + name: "plain_fn", + description: "plain", + parameters: { type: "object", properties: {} }, + }, + ]); + + expect(tools.map((tool) => `${tool.type}:${tool.name || ""}`)).toEqual([ + "tool_search:", + "namespace:codex_app", + "function:plain_fn", + ]); + }); + + it("preserves hosted Responses tools", () => { + const tools = normalizeTools([ + { type: "web_search", search_context_size: "medium" }, + { type: "image_generation", size: "1024x1024" }, + { type: "mcp", server_label: "docs", server_url: "https://example.com/mcp" }, + { type: "local_shell" }, + { type: "code_interpreter", container: { type: "auto" } }, + { type: "computer", display_width: 1024, display_height: 768, environment: "browser" }, + ]); + + expect(tools.map((tool) => tool.type)).toEqual([ + "web_search", + "image_generation", + "mcp", + "local_shell", + "code_interpreter", + "computer", + ]); + }); + + it("preserves custom freeform tools with format payloads", () => { + const tools = normalizeTools([ + { + type: "custom", + name: "apply_patch", + description: "patch", + format: { type: "grammar", syntax: "lark", definition: "start: /.+/" }, + }, + ]); + + expect(tools).toEqual([ + { + type: "custom", + name: "apply_patch", + description: "patch", + format: { type: "grammar", syntax: "lark", definition: "start: /.+/" }, + }, + ]); + }); +});