diff --git a/open-sse/executors/codex.js b/open-sse/executors/codex.js index eaac4bf651b..db3b634f5ad 100644 --- a/open-sse/executors/codex.js +++ b/open-sse/executors/codex.js @@ -414,17 +414,28 @@ export class CodexExecutor extends BaseExecutor { } // Priority: explicit reasoning.effort > reasoning_effort param > model suffix > default (medium) - if (!body.reasoning) { - const effort = body.reasoning_effort || modelEffort || 'low'; - body.reasoning = { effort, summary: "auto" }; - } else if (!body.reasoning.summary) { - body.reasoning.summary = "auto"; + if (!body.reasoning || typeof body.reasoning !== 'object' || Array.isArray(body.reasoning)) { + const effort = body.reasoning_effort || modelEffort || 'medium'; + body.reasoning = { effort, summary: 'auto' }; + } else { + if (!body.reasoning.effort) { + body.reasoning.effort = body.reasoning_effort || modelEffort || 'medium'; + } + if (!body.reasoning.summary) { + body.reasoning.summary = 'auto'; + } } delete body.reasoning_effort; // Include reasoning encrypted content (required by Codex backend for reasoning models) + const include = Array.isArray(body.include) ? body.include : []; if (body.reasoning && body.reasoning.effort && body.reasoning.effort !== 'none') { - body.include = ["reasoning.encrypted_content"]; + body.include = include.includes("reasoning.encrypted_content") + ? include + : [...include, "reasoning.encrypted_content"]; + } else if (include.length > 0) { + body.include = include.filter(item => item !== "reasoning.encrypted_content"); + if (body.include.length === 0) delete body.include; } // Remove unsupported parameters for Codex API diff --git a/open-sse/translator/request/openai-responses.js b/open-sse/translator/request/openai-responses.js index 2c329d6ec7c..e216398557d 100644 --- a/open-sse/translator/request/openai-responses.js +++ b/open-sse/translator/request/openai-responses.js @@ -309,6 +309,7 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) if (body.temperature !== undefined) result.temperature = body.temperature; if (body.max_tokens !== undefined) result.max_tokens = body.max_tokens; if (body.top_p !== undefined) result.top_p = body.top_p; + if (body.reasoning_effort !== undefined) result.reasoning_effort = body.reasoning_effort; return result; } diff --git a/tests/unit/codex-request-transform.test.js b/tests/unit/codex-request-transform.test.js new file mode 100644 index 00000000000..4e24d3b57e6 --- /dev/null +++ b/tests/unit/codex-request-transform.test.js @@ -0,0 +1,154 @@ +import { describe, expect, it } from "vitest"; +import { CodexExecutor } from "../../open-sse/executors/codex.js"; +import { openaiToOpenAIResponsesRequest } from "../../open-sse/translator/request/openai-responses.js"; + +function makeBody(overrides = {}) { + return { + input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }], + ...overrides, + }; +} + +describe("CodexExecutor request transform", () => { + it("defaults base Codex models to medium effort", () => { + const executor = new CodexExecutor(); + const out = executor.transformRequest("gpt-5.3-codex", makeBody({ model: "gpt-5.3-codex" }), true, {}); + + expect(out.model).toBe("gpt-5.3-codex"); + expect(out.reasoning).toEqual({ effort: "medium", summary: "auto" }); + expect(out.include).toEqual(["reasoning.encrypted_content"]); + expect(out.reasoning_effort).toBeUndefined(); + }); + + it("uses model effort suffix and strips suffix before upstream call", () => { + const executor = new CodexExecutor(); + const out = executor.transformRequest("gpt-5.3-codex-high", makeBody({ model: "gpt-5.3-codex-high" }), true, {}); + + expect(out.model).toBe("gpt-5.3-codex"); + expect(out.reasoning).toEqual({ effort: "high", summary: "auto" }); + }); + + it("keeps explicit reasoning.effort over model suffix", () => { + const executor = new CodexExecutor(); + const out = executor.transformRequest( + "gpt-5.3-codex-high", + makeBody({ model: "gpt-5.3-codex-high", reasoning: { effort: "low" } }), + true, + {}, + ); + + expect(out.model).toBe("gpt-5.3-codex"); + expect(out.reasoning).toEqual({ effort: "low", summary: "auto" }); + }); + + it("applies reasoning_effort when reasoning object lacks effort", () => { + const executor = new CodexExecutor(); + const out = executor.transformRequest( + "gpt-5.3-codex", + makeBody({ model: "gpt-5.3-codex", reasoning: { summary: "detailed" }, reasoning_effort: "xhigh" }), + true, + {}, + ); + + expect(out.reasoning).toEqual({ effort: "xhigh", summary: "detailed" }); + expect(out.reasoning_effort).toBeUndefined(); + }); + + it("preserves Chat Completions reasoning_effort through OpenAI Responses translation", () => { + const translated = openaiToOpenAIResponsesRequest( + "gpt-5.3-codex", + { + model: "gpt-5.3-codex", + messages: [{ role: "user", content: "hi" }], + reasoning_effort: "high", + }, + true, + null, + ); + + const executor = new CodexExecutor(); + const out = executor.transformRequest("gpt-5.3-codex", translated, true, {}); + + expect(out.reasoning).toEqual({ effort: "high", summary: "auto" }); + expect(out.reasoning_effort).toBeUndefined(); + }); + + it("does not request encrypted reasoning content when effort is none", () => { + const executor = new CodexExecutor(); + const out = executor.transformRequest("gpt-5.3-codex-none", makeBody({ model: "gpt-5.3-codex-none" }), true, {}); + + expect(out.model).toBe("gpt-5.3-codex"); + expect(out.reasoning).toEqual({ effort: "none", summary: "auto" }); + expect(out.include).toBeUndefined(); + }); + + it("removes encrypted reasoning include when effort is none", () => { + const executor = new CodexExecutor(); + const out = executor.transformRequest( + "gpt-5.3-codex", + makeBody({ + model: "gpt-5.3-codex", + reasoning_effort: "none", + include: ["reasoning.encrypted_content"], + }), + true, + {}, + ); + + expect(out.reasoning).toEqual({ effort: "none", summary: "auto" }); + expect(out.include).toBeUndefined(); + }); + + it("preserves existing include values when adding encrypted reasoning content", () => { + const executor = new CodexExecutor(); + const out = executor.transformRequest( + "gpt-5.3-codex", + makeBody({ + model: "gpt-5.3-codex", + include: ["web_search_call.action.sources"], + }), + true, + {}, + ); + + expect(out.include).toEqual(["web_search_call.action.sources", "reasoning.encrypted_content"]); + }); + + it("maps review aliases before effort suffix parsing", () => { + const executor = new CodexExecutor(); + const out = executor.transformRequest( + "gpt-5.3-codex-high-review", + makeBody({ model: "gpt-5.3-codex-high-review" }), + true, + {}, + ); + + expect(out.model).toBe("gpt-5.3-codex"); + expect(out.reasoning).toEqual({ effort: "high", summary: "auto" }); + }); + + it("strips parameters Codex backend rejects", () => { + const executor = new CodexExecutor(); + const out = executor.transformRequest( + "gpt-5.3-codex", + makeBody({ + model: "gpt-5.3-codex", + max_tokens: 100, + max_completion_tokens: 100, + max_output_tokens: 100, + temperature: 0.2, + stream_options: { include_usage: true }, + previous_response_id: "resp_abc", + }), + true, + {}, + ); + + expect(out.max_tokens).toBeUndefined(); + expect(out.max_completion_tokens).toBeUndefined(); + expect(out.max_output_tokens).toBeUndefined(); + expect(out.temperature).toBeUndefined(); + expect(out.stream_options).toBeUndefined(); + expect(out.previous_response_id).toBeUndefined(); + }); +});