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
129 lines
4 KiB
JavaScript
129 lines
4 KiB
JavaScript
/**
|
|
* Normalize Responses API input to array format.
|
|
* Accepts string or array, returns array of message items.
|
|
* @param {string|Array} input - raw input from Responses API body
|
|
* @returns {Array|null} normalized array or null if invalid
|
|
*/
|
|
export function normalizeResponsesInput(input) {
|
|
if (typeof input === "string") {
|
|
const text = input.trim() === "" ? "..." : input;
|
|
return [{ type: "message", role: "user", content: [{ type: "input_text", text }] }];
|
|
}
|
|
if (Array.isArray(input)) return input;
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Convert OpenAI Responses API format to standard chat completions format
|
|
* Responses API uses: { input: [...], instructions: "..." }
|
|
* Chat API uses: { messages: [...] }
|
|
*/
|
|
export function convertResponsesApiFormat(body) {
|
|
if (!body.input) return body;
|
|
|
|
const result = { ...body };
|
|
result.messages = [];
|
|
|
|
// Convert instructions to system message
|
|
if (body.instructions) {
|
|
result.messages.push({ role: "system", content: body.instructions });
|
|
}
|
|
|
|
// Group items by conversation turn
|
|
let currentAssistantMsg = null;
|
|
let pendingToolCalls = [];
|
|
let pendingToolResults = [];
|
|
|
|
const inputItems = normalizeResponsesInput(body.input);
|
|
if (!inputItems) return body;
|
|
|
|
for (const item of inputItems) {
|
|
// Determine item type - Droid CLI sends role-based items without 'type' field
|
|
// Fallback: if no type but has role property, treat as message
|
|
const itemType = item.type || (item.role ? "message" : null);
|
|
|
|
if (itemType === "message") {
|
|
// Flush any pending assistant message with tool calls
|
|
if (currentAssistantMsg) {
|
|
result.messages.push(currentAssistantMsg);
|
|
currentAssistantMsg = null;
|
|
}
|
|
// Flush pending tool results
|
|
if (pendingToolResults.length > 0) {
|
|
for (const tr of pendingToolResults) {
|
|
result.messages.push(tr);
|
|
}
|
|
pendingToolResults = [];
|
|
}
|
|
|
|
// 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;
|
|
result.messages.push({ role: item.role, content });
|
|
}
|
|
else if (itemType === "function_call") {
|
|
// Start or append to assistant message with tool_calls
|
|
if (!currentAssistantMsg) {
|
|
currentAssistantMsg = {
|
|
role: "assistant",
|
|
content: null,
|
|
tool_calls: []
|
|
};
|
|
}
|
|
currentAssistantMsg.tool_calls.push({
|
|
id: item.call_id,
|
|
type: "function",
|
|
function: {
|
|
name: item.name,
|
|
arguments: item.arguments
|
|
}
|
|
});
|
|
}
|
|
else if (itemType === "function_call_output") {
|
|
// Flush assistant message first if exists
|
|
if (currentAssistantMsg) {
|
|
result.messages.push(currentAssistantMsg);
|
|
currentAssistantMsg = null;
|
|
}
|
|
// Add tool result
|
|
pendingToolResults.push({
|
|
role: "tool",
|
|
tool_call_id: item.call_id,
|
|
content: typeof item.output === "string" ? item.output : JSON.stringify(item.output)
|
|
});
|
|
}
|
|
else if (itemType === "reasoning") {
|
|
// Skip reasoning items - they are for display only
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Flush remaining
|
|
if (currentAssistantMsg) {
|
|
result.messages.push(currentAssistantMsg);
|
|
}
|
|
if (pendingToolResults.length > 0) {
|
|
for (const tr of pendingToolResults) {
|
|
result.messages.push(tr);
|
|
}
|
|
}
|
|
|
|
// Cleanup Responses API specific fields
|
|
delete result.input;
|
|
delete result.instructions;
|
|
delete result.include;
|
|
delete result.prompt_cache_key;
|
|
delete result.store;
|
|
delete result.reasoning;
|
|
|
|
return result;
|
|
}
|