import { ROLE, OPENAI_BLOCK, RESPONSES_ITEM } from "../schema/index.js"; /** * Normalize Responses API input to array format. * Accepts string or array, returns array of message items. * An empty array is treated like an empty string — providers require at least one user * message, so we inject a placeholder rather than forwarding an empty messages[]. * @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: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text }] }]; } if (Array.isArray(input)) { // Empty input[] would produce messages:[] which all providers reject (#389) if (input.length === 0) { return [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "..." }] }]; } 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: 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 ? RESPONSES_ITEM.MESSAGE : null); if (itemType === RESPONSES_ITEM.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 === RESPONSES_ITEM.INPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text }; if (c.type === RESPONSES_ITEM.OUTPUT_TEXT) return { type: OPENAI_BLOCK.TEXT, text: c.text }; if (c.type === RESPONSES_ITEM.INPUT_IMAGE) { const url = c.image_url || c.file_id || ""; return { type: OPENAI_BLOCK.IMAGE_URL, image_url: { url, detail: c.detail || "auto" } }; } return c; }) : item.content; result.messages.push({ role: item.role, content }); } else if (itemType === RESPONSES_ITEM.FUNCTION_CALL) { // Start or append to assistant message with tool_calls if (!currentAssistantMsg) { currentAssistantMsg = { role: ROLE.ASSISTANT, content: null, tool_calls: [] }; } // Skip items with empty/missing name — upstream APIs reject nameless tool calls (#444) if (!item.name || typeof item.name !== "string" || item.name.trim() === "") continue; currentAssistantMsg.tool_calls.push({ id: item.call_id, type: OPENAI_BLOCK.FUNCTION, function: { name: item.name, arguments: item.arguments } }); } else if (itemType === RESPONSES_ITEM.FUNCTION_CALL_OUTPUT) { // Flush assistant message first if exists if (currentAssistantMsg) { result.messages.push(currentAssistantMsg); currentAssistantMsg = null; } // Add tool result pendingToolResults.push({ role: ROLE.TOOL, tool_call_id: item.call_id, content: typeof item.output === "string" ? item.output : JSON.stringify(item.output) }); } else if (itemType === RESPONSES_ITEM.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; }