From 40a53fbd3398196baf3a17eef584ce32b3ff6276 Mon Sep 17 00:00:00 2001 From: Rodrigo Rodrigues Costa Date: Thu, 5 Mar 2026 00:31:50 -0300 Subject: [PATCH] Fix: Codex image support - convert image_url to input_image format (#236) 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 --- open-sse/executors/codex.js | 15 +++++++++++++++ open-sse/translator/helpers/responsesApiHelper.js | 6 +++++- open-sse/translator/request/openai-responses.js | 15 +++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/open-sse/executors/codex.js b/open-sse/executors/codex.js index 0c0ee6d6..e49edf57 100644 --- a/open-sse/executors/codex.js +++ b/open-sse/executors/codex.js @@ -34,6 +34,21 @@ export class CodexExecutor extends BaseExecutor { 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; diff --git a/open-sse/translator/helpers/responsesApiHelper.js b/open-sse/translator/helpers/responsesApiHelper.js index 5fff8bd6..8254042c 100644 --- a/open-sse/translator/helpers/responsesApiHelper.js +++ b/open-sse/translator/helpers/responsesApiHelper.js @@ -56,11 +56,15 @@ export function convertResponsesApiFormat(body) { pendingToolResults = []; } - // Convert content: input_text → text, output_text → text + // Convert content: input_text → text, output_text → text, input_image → image_url const content = Array.isArray(item.content) ? item.content.map(c => { if (c.type === "input_text") return { type: "text", text: c.text }; if (c.type === "output_text") return { type: "text", text: c.text }; + if (c.type === "input_image") { + const url = c.image_url || c.file_id || ""; + return { type: "image_url", image_url: { url, detail: c.detail || "auto" } }; + } return c; }) : item.content; diff --git a/open-sse/translator/request/openai-responses.js b/open-sse/translator/request/openai-responses.js index 9b6117cb..ab919971 100644 --- a/open-sse/translator/request/openai-responses.js +++ b/open-sse/translator/request/openai-responses.js @@ -48,11 +48,15 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials) pendingToolResults = []; } - // Convert content: input_text → text, output_text → text + // Convert content: input_text → text, output_text → text, input_image → image_url const content = Array.isArray(item.content) ? item.content.map(c => { if (c.type === "input_text") return { type: "text", text: c.text }; if (c.type === "output_text") return { type: "text", text: c.text }; + if (c.type === "input_image") { + const url = c.image_url || c.file_id || ""; + return { type: "image_url", image_url: { url, detail: c.detail || "auto" } }; + } return c; }) : item.content; @@ -186,7 +190,14 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) : Array.isArray(msg.content) ? msg.content.map(c => { if (c.type === "text") return { type: contentType, text: c.text }; - if (c.type === "image_url") return { type: "image_url", image_url: c.image_url }; + // Convert Chat Completions image_url → Responses API input_image + // Responses API expects: { type: "input_image", image_url: "" } + // Chat Completions sends: { type: "image_url", image_url: { url: "...", detail: "..." } } + 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" }; + } + if (c.type === "input_image") return c; // Serialize any unknown type (tool_use, tool_result, thinking, etc.) as text const text = c.text || c.content || JSON.stringify(c); return { type: contentType, text: typeof text === "string" ? text : JSON.stringify(text) };