Cursor sends images as Chat Completions format:
{ type: "image_url", image_url: { url: "data:...", detail: "auto" } }
But Codex Responses API requires:
{ type: "input_image", image_url: "data:..." }
- openai-responses.js: bidirectional conversion image_url <-> input_image
- responsesApiHelper.js: input_image -> image_url in Responses->Chat path
- codex.js: safety net conversion in executor before sending to Codex API
Note: Cursor has a known bug where images bypass the Override OpenAI Base URL
and are sent directly to api.openai.com. This fix is effective for other clients
(curl, Codex CLI, Claude Code) that route through the proxy correctly.
Made-with: Cursor
108 lines
4 KiB
JavaScript
108 lines
4 KiB
JavaScript
import { BaseExecutor } from "./base.js";
|
|
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
|
|
import { PROVIDERS } from "../config/constants.js";
|
|
import { normalizeResponsesInput } from "../translator/helpers/responsesApiHelper.js";
|
|
|
|
/**
|
|
* Codex Executor - handles OpenAI Codex API (Responses API format)
|
|
* Automatically injects default instructions if missing
|
|
*/
|
|
export class CodexExecutor extends BaseExecutor {
|
|
constructor() {
|
|
super("codex", PROVIDERS.codex);
|
|
}
|
|
|
|
/**
|
|
* Override headers to add session_id per request
|
|
*/
|
|
buildHeaders(credentials, stream = true) {
|
|
const headers = super.buildHeaders(credentials, stream);
|
|
headers["session_id"] = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
return headers;
|
|
}
|
|
|
|
/**
|
|
* Transform request before sending - inject default instructions if missing
|
|
*/
|
|
transformRequest(model, body, stream, credentials) {
|
|
// Convert string input to array format (Codex API requires input as array)
|
|
const normalized = normalizeResponsesInput(body.input);
|
|
if (normalized) body.input = normalized;
|
|
|
|
// Ensure input is present and non-empty (Codex API rejects empty input)
|
|
if (!body.input || (Array.isArray(body.input) && body.input.length === 0)) {
|
|
body.input = [{ type: "message", role: "user", content: [{ type: "input_text", text: "..." }] }];
|
|
}
|
|
|
|
// Normalize image content: image_url → input_image (Responses API format)
|
|
if (Array.isArray(body.input)) {
|
|
for (const item of body.input) {
|
|
if (Array.isArray(item.content)) {
|
|
item.content = item.content.map(c => {
|
|
if (c.type === "image_url") {
|
|
const url = typeof c.image_url === "string" ? c.image_url : c.image_url?.url;
|
|
return { type: "input_image", image_url: url, detail: c.image_url?.detail || "auto" };
|
|
}
|
|
return c;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure streaming is enabled (Codex API requires it)
|
|
body.stream = true;
|
|
|
|
// If no instructions provided, inject default Codex instructions
|
|
if (!body.instructions || body.instructions.trim() === "") {
|
|
body.instructions = CODEX_DEFAULT_INSTRUCTIONS;
|
|
}
|
|
|
|
// Ensure store is false (Codex requirement)
|
|
body.store = false;
|
|
|
|
// Extract thinking level from model name suffix
|
|
// e.g., gpt-5.3-codex-high → high, gpt-5.3-codex → medium (default)
|
|
const effortLevels = ['none', 'low', 'medium', 'high', 'xhigh'];
|
|
let modelEffort = null;
|
|
for (const level of effortLevels) {
|
|
if (model.endsWith(`-${level}`)) {
|
|
modelEffort = level;
|
|
// Strip suffix from model name for actual API call
|
|
body.model = body.model.replace(`-${level}`, '');
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Priority: explicit reasoning.effort > reasoning_effort param > model suffix > default (medium)
|
|
if (!body.reasoning) {
|
|
const effort = body.reasoning_effort || modelEffort || 'medium';
|
|
body.reasoning = { effort, summary: "auto" };
|
|
} else if (!body.reasoning.summary) {
|
|
body.reasoning.summary = "auto";
|
|
}
|
|
delete body.reasoning_effort;
|
|
|
|
// Include reasoning encrypted content (required by Codex backend for reasoning models)
|
|
if (body.reasoning && body.reasoning.effort && body.reasoning.effort !== 'none') {
|
|
body.include = ["reasoning.encrypted_content"];
|
|
}
|
|
|
|
// 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;
|
|
delete body.max_tokens;
|
|
delete body.user; // Cursor sends this but Codex doesn't support it
|
|
delete body.prompt_cache_retention; // 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
|
|
|
|
return body;
|
|
}
|
|
}
|