Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 29 additions & 15 deletions open-sse/executors/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

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
// 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",
]);

for (const key of Object.keys(body)) {
if (!RESPONSES_API_ALLOWLIST.has(key)) {
delete body[key];
}
}

return body;
}
Expand Down
70 changes: 70 additions & 0 deletions tests/unit/executor-codex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down