refactor(open-sse): translator DRY + schema enums, bug fixes, dead code cleanup
- Bug B1-B7: media UI m.kind||m.type, serviceKinds, gemini mediaPriority, schema kind, models/info lookup by kind - Dead code D1-D6: safeParseJSON, drop PROVIDER_ENDPOINTS, orphan fetcher, GITHUB_CONFIG derive, getProviderConfig internal, legacy kiro file - Translator concerns: toOpenAIUsage, toOpenAIFinish (gemini/kiro/ollama + fix kiro tool finish), thinking effort maps - Reorg helpers/ → concerns/ (logic) + formats/ (per-format) + schema/ (pure enums: roles/blocks/finishReasons/defaults) - Wire ~280 hardcoded role/block/finish/default literals to schema enums across 20+ files - collapseTextParts + extractTextContent dedup - Normalize translator fn names to openaiToXRequest / xToOpenAIResponse - Golden tests lock behavior; 0 regression (byte-for-byte providers/alias, 26=26 known fails) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
141
open-sse/translator/formats/responsesApi.js
Normal file
141
open-sse/translator/formats/responsesApi.js
Normal file
@@ -0,0 +1,141 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user