From f52df7b8248f94999e2bb2b30a3aa5900b1e9914 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sat, 23 May 2026 03:45:06 -0300 Subject: [PATCH] fix(codex): use allowlist to strip non-Responses-API fields in non-passthrough path (#2608) (#2615) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.3 — fix(codex): allowlist-based sanitization for gpt-5.5 Responses API --- open-sse/executors/codex.ts | 42 ++++++++++++------- tests/unit/executor-codex.test.ts | 70 +++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 14 deletions(-) diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 70c596d55d..d3dddc7e7e 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -1286,21 +1286,35 @@ export class CodexExecutor extends BaseExecutor { return body; } - // Remove unsupported parameters for Codex API - delete body.temperature; - delete body.top_p; - delete body.frequency_penalty; - delete body.presence_penalty; - delete body.logprobs; - delete body.top_logprobs; - delete body.n; - delete body.seed; - // max_tokens and max_output_tokens already deleted above (before passthrough return) - delete body.user; // Cursor sends this but Codex doesn't support it + // Issue #2608: Use an allowlist of known Responses API fields instead of a + // denylist of Chat Completions fields. The denylist approach missed fields + // like `stop`, `response_format`, `logit_bias`, `function_call`, `functions`, + // `max_completion_tokens`, and `parallel_tool_calls` — causing gpt-5.5 to + // reject with "routing_unsupported" (400). An allowlist is future-proof: + // any unknown field from Chat Completions (or other formats) is stripped. + const RESPONSES_API_ALLOWLIST = new Set([ + "model", + "input", + "instructions", + "tools", + "tool_choice", + "stream", + "store", + "reasoning", + "service_tier", + "include", + "previous_response_id", + "prompt_cache_key", + "client_metadata", + // Internal markers used by OmniRoute pipeline + "_omnirouteResponsesStore", + ]); - delete body.metadata; // Cursor sends this but Codex doesn't support it - delete body.stream_options; // Cursor sends this but Codex doesn't support it - delete body.safety_identifier; // Droid CLI sends this but Codex doesn't support it + for (const key of Object.keys(body)) { + if (!RESPONSES_API_ALLOWLIST.has(key)) { + delete body[key]; + } + } return body; } diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index 975cd1902a..56d7da99ee 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -216,6 +216,76 @@ test("CodexExecutor.transformRequest injects default instructions, clamps reason assert.equal(result.stream_options, undefined); }); +// Issue #2608: gpt-5.5 models reject residual Chat Completions fields via Codex OAuth. +// The non-passthrough path must strip ALL non-Responses-API fields using an allowlist. +test("CodexExecutor.transformRequest non-passthrough allowlist strips all residual Chat Completions fields (#2608)", () => { + const executor = new CodexExecutor(); + const body = { + model: "gpt-5.5", + messages: [{ role: "user", content: "hello" }], + instructions: "", + // All of these are Chat Completions fields that must be stripped: + temperature: 0.7, + top_p: 0.9, + frequency_penalty: 0.5, + presence_penalty: 0.3, + logprobs: true, + top_logprobs: 3, + n: 2, + seed: 42, + stop: ["\n"], + response_format: { type: "json_object" }, + logit_bias: { "123": 1 }, + function_call: "auto", + functions: [{ name: "test", parameters: {} }], + max_completion_tokens: 1000, + parallel_tool_calls: true, + user: "cursor-user", + metadata: { key: "value" }, + stream_options: { include_usage: true }, + safety_identifier: "safe-1", + suffix: "end", + // Custom/arbitrary fields that could be injected by middleware + custom_field: "should be stripped", + _internal_marker: true, + }; + + const result = executor.transformRequest("gpt-5.5", body, false, { + requestEndpointPath: "/responses", + }); + + // Allowed Responses API fields should survive + assert.equal(result.model, "gpt-5.5"); + assert.ok(Array.isArray(result.input)); + assert.equal(typeof result.instructions, "string"); + assert.equal(result.store, false); + assert.equal(result.stream, true); + + // All Chat Completions fields must be stripped + assert.equal(result.temperature, undefined, "temperature should be stripped"); + assert.equal(result.top_p, undefined, "top_p should be stripped"); + assert.equal(result.frequency_penalty, undefined, "frequency_penalty should be stripped"); + assert.equal(result.presence_penalty, undefined, "presence_penalty should be stripped"); + assert.equal(result.logprobs, undefined, "logprobs should be stripped"); + assert.equal(result.top_logprobs, undefined, "top_logprobs should be stripped"); + assert.equal(result.n, undefined, "n should be stripped"); + assert.equal(result.seed, undefined, "seed should be stripped"); + assert.equal(result.stop, undefined, "stop should be stripped"); + assert.equal(result.response_format, undefined, "response_format should be stripped"); + assert.equal(result.logit_bias, undefined, "logit_bias should be stripped"); + assert.equal(result.function_call, undefined, "function_call should be stripped"); + assert.equal(result.functions, undefined, "functions should be stripped"); + assert.equal(result.max_completion_tokens, undefined, "max_completion_tokens should be stripped"); + assert.equal(result.parallel_tool_calls, undefined, "parallel_tool_calls should be stripped"); + assert.equal(result.user, undefined, "user should be stripped"); + assert.equal(result.metadata, undefined, "metadata should be stripped"); + assert.equal(result.stream_options, undefined, "stream_options should be stripped"); + assert.equal(result.safety_identifier, undefined, "safety_identifier should be stripped"); + assert.equal(result.suffix, undefined, "suffix should be stripped"); + assert.equal(result.custom_field, undefined, "arbitrary custom fields should be stripped"); + assert.equal(result._internal_marker, undefined, "internal markers should be stripped"); +}); + test("CodexExecutor.transformRequest normalizes max reasoning_effort to xhigh", () => { const executor = new CodexExecutor(); const result = executor.transformRequest(