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:
decolua
2026-06-14 18:49:38 +07:00
parent c5c9061eac
commit d3f61aac2f
145 changed files with 1252 additions and 1160 deletions

View File

@@ -1,7 +1,10 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
import { encodeDataUri } from "../helpers/imageHelper.js";
import { adjustMaxTokens } from "../formats/maxTokens.js";
import { encodeDataUri } from "../concerns/image.js";
import { ROLE, GEMINI_ROLE, OPENAI_BLOCK } from "../schema/index.js";
import { budgetToEffort } from "../concerns/thinking.js";
import { collapseTextParts } from "../concerns/message.js";
// Convert Antigravity request to OpenAI format
// Antigravity body: { project, model, userAgent, requestType, requestId, request: { contents, systemInstruction, tools, toolConfig, generationConfig, sessionId } }
@@ -32,16 +35,8 @@ export function antigravityToOpenAIRequest(model, body, stream) {
// Thinking config → reasoning_effort
if (config.thinkingConfig) {
const budget = config.thinkingConfig.thinkingBudget || 0;
if (budget > 0) {
if (budget <= 2048) {
result.reasoning_effort = "low";
} else if (budget <= 16384) {
result.reasoning_effort = "medium";
} else {
result.reasoning_effort = "high";
}
}
const effort = budgetToEffort(config.thinkingConfig.thinkingBudget || 0);
if (effort) result.reasoning_effort = effort;
}
}
@@ -49,7 +44,7 @@ export function antigravityToOpenAIRequest(model, body, stream) {
if (req.systemInstruction) {
const systemText = extractText(req.systemInstruction);
if (systemText) {
result.messages.push({ role: "system", content: systemText });
result.messages.push({ role: ROLE.SYSTEM, content: systemText });
}
}
@@ -74,7 +69,7 @@ export function antigravityToOpenAIRequest(model, body, stream) {
if (tool.functionDeclarations) {
for (const func of tool.functionDeclarations) {
result.tools.push({
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name: func.name,
description: func.description || "",
@@ -123,7 +118,7 @@ function normalizeSchemaTypes(schema) {
// Convert Antigravity content to OpenAI message
// Handles: text, thought, thoughtSignature, functionCall, functionResponse, inlineData
function convertContent(content) {
const role = content.role === "model" ? "assistant" : content.role === "user" ? "user" : content.role;
const role = content.role === GEMINI_ROLE.MODEL ? ROLE.ASSISTANT : content.role === GEMINI_ROLE.USER ? ROLE.USER : content.role;
if (!content.parts || !Array.isArray(content.parts)) {
return null;
@@ -143,19 +138,19 @@ function convertContent(content) {
// Text with thoughtSignature = regular text after thinking
if (part.thoughtSignature && part.text !== undefined) {
textParts.push({ type: "text", text: part.text });
textParts.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
continue;
}
// Regular text
if (part.text !== undefined) {
textParts.push({ type: "text", text: part.text });
textParts.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
}
// Inline data (images)
if (part.inlineData) {
textParts.push({
type: "image_url",
type: OPENAI_BLOCK.IMAGE_URL,
image_url: {
url: encodeDataUri(part.inlineData.mimeType, part.inlineData.data)
}
@@ -166,7 +161,7 @@ function convertContent(content) {
if (part.functionCall) {
toolCalls.push({
id: part.functionCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name: part.functionCall.name,
arguments: JSON.stringify(part.functionCall.args || {})
@@ -177,7 +172,7 @@ function convertContent(content) {
// Function response → collect all, each becomes a separate tool message
if (part.functionResponse) {
toolResults.push({
role: "tool",
role: ROLE.TOOL,
tool_call_id: part.functionResponse.id || part.functionResponse.name,
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
});
@@ -191,9 +186,9 @@ function convertContent(content) {
// Assistant with tool calls
if (toolCalls.length > 0) {
const msg = { role: "assistant" };
const msg = { role: ROLE.ASSISTANT };
if (textParts.length > 0) {
msg.content = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts;
msg.content = collapseTextParts(textParts);
}
if (reasoningContent) {
msg.reasoning_content = reasoningContent;
@@ -206,7 +201,7 @@ function convertContent(content) {
if (textParts.length > 0 || reasoningContent) {
const msg = { role };
if (textParts.length > 0) {
msg.content = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts;
msg.content = collapseTextParts(textParts);
}
if (reasoningContent) {
msg.reasoning_content = reasoningContent;

View File

@@ -1,7 +1,9 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
import { encodeDataUri } from "../helpers/imageHelper.js";
import { adjustMaxTokens } from "../formats/maxTokens.js";
import { encodeDataUri } from "../concerns/image.js";
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
import { collapseTextParts } from "../concerns/message.js";
function stripAnthropicBillingHeader(text) {
if (typeof text !== "string") return "";
@@ -34,7 +36,7 @@ export function claudeToOpenAIRequest(model, body, stream) {
if (systemContent) {
result.messages.push({
role: "system",
role: ROLE.SYSTEM,
content: systemContent
});
}
@@ -58,13 +60,13 @@ export function claudeToOpenAIRequest(model, body, stream) {
// Fix missing tool responses - OpenAI requires every tool_call to have a response.
// Local variant: scans contiguous tool replies + inserts "[No response received]"
// (distinct from the global immediate-next check in toolCallHelper, runs on the openai leg).
// (distinct from the global immediate-next check in concerns/toolCall, runs on the openai leg).
fixMissingToolResponsesOpenAI(result.messages);
// Tools
if (body.tools && Array.isArray(body.tools)) {
result.tools = body.tools.map(tool => ({
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name: tool.name,
description: String(tool.description || ""),
@@ -85,7 +87,7 @@ export function claudeToOpenAIRequest(model, body, stream) {
function fixMissingToolResponsesOpenAI(messages) {
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
if (msg.role === ROLE.ASSISTANT && msg.tool_calls && msg.tool_calls.length > 0) {
const toolCallIds = msg.tool_calls.map(tc => tc.id);
// Collect all tool response IDs that IMMEDIATELY follow this assistant message
@@ -93,7 +95,7 @@ function fixMissingToolResponsesOpenAI(messages) {
let insertPosition = i + 1;
for (let j = i + 1; j < messages.length; j++) {
const nextMsg = messages[j];
if (nextMsg.role === "tool" && nextMsg.tool_call_id) {
if (nextMsg.role === ROLE.TOOL && nextMsg.tool_call_id) {
respondedIds.add(nextMsg.tool_call_id);
insertPosition = j + 1;
} else {
@@ -106,7 +108,7 @@ function fixMissingToolResponsesOpenAI(messages) {
if (missingIds.length > 0) {
const missingResponses = missingIds.map(id => ({
role: "tool",
role: ROLE.TOOL,
tool_call_id: id,
content: "[No response received]"
}));
@@ -119,7 +121,7 @@ function fixMissingToolResponsesOpenAI(messages) {
// Convert single Claude message - returns single message or array of messages
function convertClaudeMessage(msg) {
const role = msg.role === "user" || msg.role === "tool" ? "user" : "assistant";
const role = msg.role === ROLE.USER || msg.role === ROLE.TOOL ? ROLE.USER : ROLE.ASSISTANT;
// Simple string content
if (typeof msg.content === "string") {
@@ -134,14 +136,14 @@ function convertClaudeMessage(msg) {
for (const block of msg.content) {
switch (block.type) {
case "text":
parts.push({ type: "text", text: block.text });
case CLAUDE_BLOCK.TEXT:
parts.push({ type: OPENAI_BLOCK.TEXT, text: block.text });
break;
case "image":
case CLAUDE_BLOCK.IMAGE:
if (block.source?.type === "base64") {
parts.push({
type: "image_url",
type: OPENAI_BLOCK.IMAGE_URL,
image_url: {
url: encodeDataUri(block.source.media_type, block.source.data)
}
@@ -149,10 +151,10 @@ function convertClaudeMessage(msg) {
}
break;
case "tool_use":
case CLAUDE_BLOCK.TOOL_USE:
toolCalls.push({
id: block.id,
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name: block.name,
arguments: JSON.stringify(block.input || {})
@@ -160,13 +162,13 @@ function convertClaudeMessage(msg) {
});
break;
case "tool_result":
case CLAUDE_BLOCK.TOOL_RESULT:
let resultContent = "";
if (typeof block.content === "string") {
resultContent = block.content;
} else if (Array.isArray(block.content)) {
resultContent = block.content
.filter(c => c.type === "text")
.filter(c => c.type === CLAUDE_BLOCK.TEXT)
.map(c => c.text)
.join("\n") || JSON.stringify(block.content);
} else if (block.content) {
@@ -174,7 +176,7 @@ function convertClaudeMessage(msg) {
}
toolResults.push({
role: "tool",
role: ROLE.TOOL,
tool_call_id: block.tool_use_id,
content: resultContent
});
@@ -185,21 +187,16 @@ function convertClaudeMessage(msg) {
// If has tool results, return array of tool messages
if (toolResults.length > 0) {
if (parts.length > 0) {
const textContent = parts.length === 1 && parts[0].type === "text"
? parts[0].text
: parts;
return [...toolResults, { role: "user", content: textContent }];
return [...toolResults, { role: ROLE.USER, content: collapseTextParts(parts) }];
}
return toolResults;
}
// If has tool calls, return assistant message with tool_calls
if (toolCalls.length > 0) {
const result = { role: "assistant" };
const result = { role: ROLE.ASSISTANT };
if (parts.length > 0) {
result.content = parts.length === 1 && parts[0].type === "text"
? parts[0].text
: parts;
result.content = collapseTextParts(parts);
}
result.tool_calls = toolCalls;
return result;
@@ -209,7 +206,7 @@ function convertClaudeMessage(msg) {
if (parts.length > 0) {
return {
role,
content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts
content: collapseTextParts(parts)
};
}
@@ -230,7 +227,7 @@ function convertToolChoice(choice) {
switch (choice.type) {
case "auto": return "auto";
case "any": return "required";
case "tool": return { type: "function", function: { name: choice.name } };
case "tool": return { type: OPENAI_BLOCK.FUNCTION, function: { name: choice.name } };
default: return "auto";
}
}

View File

@@ -1,7 +1,9 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
import { encodeDataUri } from "../helpers/imageHelper.js";
import { adjustMaxTokens } from "../formats/maxTokens.js";
import { encodeDataUri } from "../concerns/image.js";
import { collapseTextParts } from "../concerns/message.js";
import { ROLE, GEMINI_ROLE, OPENAI_BLOCK } from "../schema/index.js";
// Convert Gemini request to OpenAI format
export function geminiToOpenAIRequest(model, body, stream) {
@@ -31,7 +33,7 @@ export function geminiToOpenAIRequest(model, body, stream) {
const systemText = extractGeminiText(body.systemInstruction);
if (systemText) {
result.messages.push({
role: "system",
role: ROLE.SYSTEM,
content: systemText
});
}
@@ -54,7 +56,7 @@ export function geminiToOpenAIRequest(model, body, stream) {
if (tool.functionDeclarations) {
for (const func of tool.functionDeclarations) {
result.tools.push({
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name: func.name,
description: func.description || "",
@@ -71,7 +73,7 @@ export function geminiToOpenAIRequest(model, body, stream) {
// Convert Gemini content to OpenAI message
function convertGeminiContent(content) {
const role = content.role === "user" ? "user" : "assistant";
const role = content.role === GEMINI_ROLE.USER ? ROLE.USER : ROLE.ASSISTANT;
if (!content.parts || !Array.isArray(content.parts)) {
return null;
@@ -82,12 +84,12 @@ function convertGeminiContent(content) {
for (const part of content.parts) {
if (part.text !== undefined) {
parts.push({ type: "text", text: part.text });
parts.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
}
if (part.inlineData) {
parts.push({
type: "image_url",
type: OPENAI_BLOCK.IMAGE_URL,
image_url: {
url: encodeDataUri(part.inlineData.mimeType, part.inlineData.data)
}
@@ -97,7 +99,7 @@ function convertGeminiContent(content) {
if (part.functionCall) {
toolCalls.push({
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name: part.functionCall.name,
arguments: JSON.stringify(part.functionCall.args || {})
@@ -107,7 +109,7 @@ function convertGeminiContent(content) {
if (part.functionResponse) {
return {
role: "tool",
role: ROLE.TOOL,
tool_call_id: part.functionResponse.id || part.functionResponse.name,
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
};
@@ -115,7 +117,7 @@ function convertGeminiContent(content) {
}
if (toolCalls.length > 0) {
const result = { role: "assistant" };
const result = { role: ROLE.ASSISTANT };
if (parts.length > 0) {
result.content = parts.length === 1 ? parts[0].text : parts;
}
@@ -126,7 +128,7 @@ function convertGeminiContent(content) {
if (parts.length > 0) {
return {
role,
content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts
content: collapseTextParts(parts)
};
}

View File

@@ -6,7 +6,8 @@
*/
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { normalizeResponsesInput } from "../helpers/responsesApiHelper.js";
import { normalizeResponsesInput } from "../formats/responsesApi.js";
import { ROLE, OPENAI_BLOCK, RESPONSES_ITEM } from "../schema/index.js";
// Responses API enforces max 64 chars on call_id (#393)
const MAX_CALL_ID_LEN = 64;
@@ -23,7 +24,7 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
// Convert instructions to system message
if (body.instructions) {
result.messages.push({ role: "system", content: body.instructions });
result.messages.push({ role: ROLE.SYSTEM, content: body.instructions });
}
// Group items by conversation turn
@@ -50,9 +51,9 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
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);
const itemType = item.type || (item.role ? RESPONSES_ITEM.MESSAGE : null);
if (itemType === "message") {
if (itemType === RESPONSES_ITEM.MESSAGE) {
// Flush any pending assistant message with tool calls
if (currentAssistantMsg) {
result.messages.push(currentAssistantMsg);
@@ -69,28 +70,28 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
// 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") {
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: "image_url", image_url: { url, detail: c.detail || "auto" } };
return { type: OPENAI_BLOCK.IMAGE_URL, image_url: { url, detail: c.detail || "auto" } };
}
return c;
})
: item.content;
const msg = { role: item.role, content };
// Attach buffered reasoning to assistant turn (required by xiaomi-mimo thinking mode)
if (item.role === "assistant" && pendingReasoning) {
if (item.role === ROLE.ASSISTANT && pendingReasoning) {
msg.reasoning_content = pendingReasoning;
}
pendingReasoning = "";
result.messages.push(msg);
}
else if (itemType === "function_call") {
else if (itemType === RESPONSES_ITEM.FUNCTION_CALL) {
// Start or append to assistant message with tool_calls
if (!currentAssistantMsg) {
currentAssistantMsg = {
role: "assistant",
role: ROLE.ASSISTANT,
content: null,
tool_calls: []
};
@@ -103,14 +104,14 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
if (!item.name || typeof item.name !== "string" || item.name.trim() === "") continue;
currentAssistantMsg.tool_calls.push({
id: item.call_id,
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name: item.name,
arguments: item.arguments
}
});
}
else if (itemType === "function_call_output") {
else if (itemType === RESPONSES_ITEM.FUNCTION_CALL_OUTPUT) {
// Flush assistant message first if exists
if (currentAssistantMsg) {
result.messages.push(currentAssistantMsg);
@@ -125,12 +126,12 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
}
// Add tool result immediately
result.messages.push({
role: "tool",
role: ROLE.TOOL,
tool_call_id: item.call_id,
content: typeof item.output === "string" ? item.output : JSON.stringify(item.output)
});
}
else if (itemType === "reasoning") {
else if (itemType === RESPONSES_ITEM.REASONING) {
// Buffer reasoning text; attached to next assistant message/function_call
const txt = extractReasoningText(item);
if (txt) pendingReasoning = pendingReasoning ? `${pendingReasoning}\n${txt}` : txt;
@@ -163,7 +164,7 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
const name = tool.name;
if (!name || typeof name !== "string" || name.trim() === "") return null;
return {
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name,
description: String(tool.description || ""),
@@ -214,7 +215,7 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
const messages = body.messages || [];
for (const msg of messages) {
if (msg.role === "system") {
if (msg.role === ROLE.SYSTEM) {
// Use first system message as instructions
if (!hasSystemMessage) {
result.instructions = typeof msg.content === "string" ? msg.content : "";
@@ -224,21 +225,21 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
}
// Convert user/assistant messages to input items
if (msg.role === "user" || msg.role === "assistant") {
const contentType = msg.role === "user" ? "input_text" : "output_text";
if (msg.role === ROLE.USER || msg.role === ROLE.ASSISTANT) {
const contentType = msg.role === ROLE.USER ? RESPONSES_ITEM.INPUT_TEXT : RESPONSES_ITEM.OUTPUT_TEXT;
const content = typeof msg.content === "string"
? [{ type: contentType, text: msg.content }]
: Array.isArray(msg.content)
? msg.content.map(c => {
if (c.type === "text") return { type: contentType, text: c.text };
if (c.type === OPENAI_BLOCK.TEXT) return { type: contentType, text: c.text };
// Convert Chat Completions image_url → Responses API input_image
// Responses API expects: { type: "input_image", image_url: "<url string>" }
// Chat Completions sends: { type: "image_url", image_url: { url: "...", detail: "..." } }
if (c.type === "image_url") {
if (c.type === OPENAI_BLOCK.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 { type: RESPONSES_ITEM.INPUT_IMAGE, image_url: url, detail: c.image_url?.detail || "auto" };
}
if (c.type === "input_image") return c;
if (c.type === RESPONSES_ITEM.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) };
@@ -250,7 +251,7 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
// message block in that case; the tool_calls are pushed separately below.
if (content.length > 0) {
result.input.push({
type: "message",
type: RESPONSES_ITEM.MESSAGE,
role: msg.role,
content
});
@@ -258,10 +259,10 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
}
// Convert tool calls
if (msg.role === "assistant" && msg.tool_calls) {
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) {
for (const tc of msg.tool_calls) {
result.input.push({
type: "function_call",
type: RESPONSES_ITEM.FUNCTION_CALL,
call_id: clampCallId(tc.id),
name: tc.function?.name || "_unknown",
arguments: tc.function?.arguments || "{}"
@@ -270,14 +271,14 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
}
// Convert tool results - output must be a string for Responses API
if (msg.role === "tool") {
if (msg.role === ROLE.TOOL) {
const output = typeof msg.content === "string"
? msg.content
: Array.isArray(msg.content)
? msg.content.map(c => c.text || JSON.stringify(c)).join("")
: JSON.stringify(msg.content);
result.input.push({
type: "function_call_output",
type: RESPONSES_ITEM.FUNCTION_CALL_OUTPUT,
call_id: clampCallId(msg.tool_call_id),
output
});
@@ -292,9 +293,9 @@ export function openaiToOpenAIResponsesRequest(model, body, stream, credentials)
// Convert tools format
if (body.tools && Array.isArray(body.tools)) {
result.tools = body.tools.map(tool => {
if (tool.type === "function") {
if (tool.type === OPENAI_BLOCK.FUNCTION) {
return {
type: "function",
type: OPENAI_BLOCK.FUNCTION,
name: tool.function.name,
description: String(tool.function.description || ""),
parameters: normalizeToolParameters(tool.function.parameters),

View File

@@ -1,9 +1,12 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { CLAUDE_SYSTEM_PROMPT } from "../../config/appConstants.js";
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
import { safeParseJSON } from "../helpers/jsonUtil.js";
import { parseDataUri } from "../helpers/imageHelper.js";
import { adjustMaxTokens } from "../formats/maxTokens.js";
import { safeParseJSON } from "../concerns/json.js";
import { parseDataUri } from "../concerns/image.js";
import { effortToBudget } from "../concerns/thinking.js";
import { extractTextContent } from "../formats/gemini.js";
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
// Empty prefix matches real Claude Code behavior (no tool name prefix).
// Previously "proxy_" was used but this is a detectable fingerprint difference.
@@ -31,13 +34,13 @@ export function openaiToClaudeRequest(model, body, stream) {
if (body.messages && Array.isArray(body.messages)) {
// Extract system messages
for (const msg of body.messages) {
if (msg.role === "system") {
systemParts.push(typeof msg.content === "string" ? msg.content : extractTextContent(msg.content));
if (msg.role === ROLE.SYSTEM) {
systemParts.push(typeof msg.content === "string" ? msg.content : extractTextContent(msg.content, "\n"));
}
}
// Filter out system messages for separate processing
const nonSystemMessages = body.messages.filter(m => m.role !== "system");
const nonSystemMessages = body.messages.filter(m => m.role !== ROLE.SYSTEM);
// Process messages with merging logic
// CRITICAL: tool_result must be in separate message immediately after tool_use
@@ -52,20 +55,20 @@ export function openaiToClaudeRequest(model, body, stream) {
};
for (const msg of nonSystemMessages) {
const newRole = (msg.role === "user" || msg.role === "tool") ? "user" : "assistant";
const newRole = (msg.role === ROLE.USER || msg.role === ROLE.TOOL) ? ROLE.USER : ROLE.ASSISTANT;
const blocks = getContentBlocksFromMessage(msg, toolNameMap);
const hasToolUse = blocks.some(b => b.type === "tool_use");
const hasToolResult = blocks.some(b => b.type === "tool_result");
const hasToolUse = blocks.some(b => b.type === CLAUDE_BLOCK.TOOL_USE);
const hasToolResult = blocks.some(b => b.type === CLAUDE_BLOCK.TOOL_RESULT);
// Separate tool_result from other content
if (hasToolResult) {
const toolResultBlocks = blocks.filter(b => b.type === "tool_result");
const otherBlocks = blocks.filter(b => b.type !== "tool_result");
const toolResultBlocks = blocks.filter(b => b.type === CLAUDE_BLOCK.TOOL_RESULT);
const otherBlocks = blocks.filter(b => b.type !== CLAUDE_BLOCK.TOOL_RESULT);
flushCurrentMessage();
if (toolResultBlocks.length > 0) {
result.messages.push({ role: "user", content: toolResultBlocks });
result.messages.push({ role: ROLE.USER, content: toolResultBlocks });
}
if (otherBlocks.length > 0) {
@@ -92,9 +95,9 @@ export function openaiToClaudeRequest(model, body, stream) {
// Add cache_control to last assistant message
for (let i = result.messages.length - 1; i >= 0; i--) {
const message = result.messages[i];
if (message.role === "assistant" && Array.isArray(message.content) && message.content.length > 0) {
if (message.role === ROLE.ASSISTANT && Array.isArray(message.content) && message.content.length > 0) {
// Find the last block that can have cache_control (not thinking blocks)
const validBlockTypes = ["text", "tool_use", "tool_result", "image"];
const validBlockTypes = [CLAUDE_BLOCK.TEXT, CLAUDE_BLOCK.TOOL_USE, CLAUDE_BLOCK.TOOL_RESULT, CLAUDE_BLOCK.IMAGE];
for (let j = message.content.length - 1; j >= 0; j--) {
const block = message.content[j];
if (validBlockTypes.includes(block.type)) {
@@ -123,13 +126,13 @@ Respond ONLY with the JSON object, no other text.`);
}
// System with Claude Code prompt and cache_control
const claudeCodePrompt = { type: "text", text: CLAUDE_SYSTEM_PROMPT };
const claudeCodePrompt = { type: CLAUDE_BLOCK.TEXT, text: CLAUDE_SYSTEM_PROMPT };
if (systemParts.length > 0) {
const systemText = systemParts.join("\n");
result.system = [
claudeCodePrompt,
{ type: "text", text: systemText, cache_control: { type: "ephemeral", ttl: "1h" } }
{ type: CLAUDE_BLOCK.TEXT, text: systemText, cache_control: { type: "ephemeral", ttl: "1h" } }
];
} else {
result.system = [claudeCodePrompt];
@@ -141,12 +144,12 @@ Respond ONLY with the JSON object, no other text.`);
for (const tool of body.tools) {
// Pass-through built-in tools (e.g. web_search_20250305) without prefix or conversion
const toolType = tool.type;
if (toolType && toolType !== "function") {
if (toolType && toolType !== OPENAI_BLOCK.FUNCTION) {
result.tools.push(tool);
continue;
}
const toolData = toolType === "function" && tool.function ? tool.function : tool;
const toolData = toolType === OPENAI_BLOCK.FUNCTION && tool.function ? tool.function : tool;
const originalName = toolData.name;
// Claude OAuth requires prefixed tool names to avoid conflicts
@@ -185,19 +188,11 @@ Respond ONLY with the JSON object, no other text.`);
// When client sends reasoning_effort (OpenAI format) but no explicit thinking block,
// translate to Claude's native format.
if (body.reasoning_effort && !result.thinking) {
const effortToBudget = {
none: 0,
low: 4096,
medium: 8192,
high: 16384,
xhigh: 32768,
};
const budget = effortToBudget[body.reasoning_effort.toLowerCase()];
if (budget === 0) {
// none → no thinking
} else if (budget) {
const budget = effortToBudget(body.reasoning_effort);
if (budget) {
result.thinking = { type: "enabled", budget_tokens: budget };
}
// budget === 0 (none) or undefined (unknown) → no thinking
}
// Attach toolNameMap to result for response translation
@@ -212,75 +207,75 @@ Respond ONLY with the JSON object, no other text.`);
function getContentBlocksFromMessage(msg, toolNameMap = new Map()) {
const blocks = [];
if (msg.role === "tool") {
if (msg.role === ROLE.TOOL) {
blocks.push({
type: "tool_result",
type: CLAUDE_BLOCK.TOOL_RESULT,
tool_use_id: msg.tool_call_id,
content: msg.content
});
} else if (msg.role === "user") {
} else if (msg.role === ROLE.USER) {
if (typeof msg.content === "string") {
if (msg.content) {
blocks.push({ type: "text", text: msg.content });
blocks.push({ type: CLAUDE_BLOCK.TEXT, text: msg.content });
}
} else if (Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part.type === "text" && part.text) {
blocks.push({ type: "text", text: part.text });
} else if (part.type === "tool_result") {
if (part.type === OPENAI_BLOCK.TEXT && part.text) {
blocks.push({ type: CLAUDE_BLOCK.TEXT, text: part.text });
} else if (part.type === CLAUDE_BLOCK.TOOL_RESULT) {
blocks.push({
type: "tool_result",
type: CLAUDE_BLOCK.TOOL_RESULT,
tool_use_id: part.tool_use_id,
content: part.content,
...(part.is_error && { is_error: part.is_error })
});
} else if (part.type === "image_url") {
} else if (part.type === OPENAI_BLOCK.IMAGE_URL) {
const url = part.image_url.url;
const parsed = parseDataUri(url);
if (parsed) {
blocks.push({
type: "image",
type: CLAUDE_BLOCK.IMAGE,
source: { type: "base64", media_type: parsed.mimeType, data: parsed.base64 }
});
} else if (url.startsWith("http://") || url.startsWith("https://")) {
blocks.push({
type: "image",
type: CLAUDE_BLOCK.IMAGE,
source: { type: "url", url }
});
}
} else if (part.type === "image" && part.source) {
blocks.push({ type: "image", source: part.source });
} else if (part.type === OPENAI_BLOCK.IMAGE && part.source) {
blocks.push({ type: CLAUDE_BLOCK.IMAGE, source: part.source });
}
}
}
} else if (msg.role === "assistant") {
} else if (msg.role === ROLE.ASSISTANT) {
if (Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part.type === "text" && part.text) {
blocks.push({ type: "text", text: part.text });
} else if (part.type === "tool_use") {
if (part.type === OPENAI_BLOCK.TEXT && part.text) {
blocks.push({ type: CLAUDE_BLOCK.TEXT, text: part.text });
} else if (part.type === CLAUDE_BLOCK.TOOL_USE) {
// Tool name already has prefix from tool declarations, keep as-is
blocks.push({ type: "tool_use", id: part.id, name: part.name, input: part.input });
} else if (part.type === "thinking") {
blocks.push({ type: CLAUDE_BLOCK.TOOL_USE, id: part.id, name: part.name, input: part.input });
} else if (part.type === CLAUDE_BLOCK.THINKING) {
// Include thinking block but strip cache_control (not allowed on thinking blocks)
const { cache_control, ...thinkingBlock } = part;
blocks.push(thinkingBlock);
}
}
} else if (msg.content) {
const text = typeof msg.content === "string" ? msg.content : extractTextContent(msg.content);
const text = typeof msg.content === "string" ? msg.content : extractTextContent(msg.content, "\n");
if (text) {
blocks.push({ type: "text", text });
blocks.push({ type: CLAUDE_BLOCK.TEXT, text });
}
}
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
for (const tc of msg.tool_calls) {
if (tc.type === "function") {
if (tc.type === OPENAI_BLOCK.FUNCTION) {
// Apply prefix to tool name
const toolName = CLAUDE_OAUTH_TOOL_PREFIX + tc.function.name;
blocks.push({
type: "tool_use",
type: CLAUDE_BLOCK.TOOL_USE,
id: tc.id,
name: toolName,
input: safeParseJSON(tc.function.arguments, tc.function.arguments)
@@ -325,15 +320,6 @@ function convertOpenAIToolChoice(choice) {
return { type: "auto" };
}
// Extract text from content
function extractTextContent(content) {
if (typeof content === "string") return content;
if (Array.isArray(content)) {
return content.filter(c => c.type === "text").map(c => c.text).join("\n");
}
return "";
}
// OpenAI -> Claude format for Antigravity (without system prompt modifications)
function openaiToClaudeRequestForAntigravity(model, body, stream) {
const result = openaiToClaudeRequest(model, body, stream);
@@ -369,7 +355,7 @@ function openaiToClaudeRequestForAntigravity(model, body, stream) {
}
const updatedContent = msg.content.map(block => {
if (block.type === "tool_use" && block.name && block.name.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) {
if (block.type === CLAUDE_BLOCK.TOOL_USE && block.name && block.name.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) {
return {
...block,
name: block.name.slice(CLAUDE_OAUTH_TOOL_PREFIX.length)

View File

@@ -12,6 +12,8 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { randomUUID } from "crypto";
import { ROLE, OPENAI_BLOCK } from "../schema/index.js";
import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js";
function flattenText(content) {
if (content == null) return "";
@@ -28,26 +30,26 @@ function flattenText(content) {
}
function toContentBlocks(content) {
if (content == null) return [{ type: "text", text: "" }];
if (typeof content === "string") return [{ type: "text", text: content }];
if (content == null) return [{ type: OPENAI_BLOCK.TEXT, text: "" }];
if (typeof content === "string") return [{ type: OPENAI_BLOCK.TEXT, text: content }];
if (Array.isArray(content)) {
const blocks = [];
for (const part of content) {
if (typeof part === "string") {
blocks.push({ type: "text", text: part });
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part });
} else if (part && typeof part === "object") {
if (part.type === "text" && typeof part.text === "string") {
blocks.push({ type: "text", text: part.text });
} else if (part.type === "image_url" || part.type === "image") {
blocks.push({ type: "text", text: "[image omitted]" });
if (part.type === OPENAI_BLOCK.TEXT && typeof part.text === "string") {
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
} else if (part.type === OPENAI_BLOCK.IMAGE_URL || part.type === OPENAI_BLOCK.IMAGE) {
blocks.push({ type: OPENAI_BLOCK.TEXT, text: "[image omitted]" });
} else if (typeof part.text === "string") {
blocks.push({ type: "text", text: part.text });
blocks.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
}
}
}
return blocks.length ? blocks : [{ type: "text", text: "" }];
return blocks.length ? blocks : [{ type: OPENAI_BLOCK.TEXT, text: "" }];
}
return [{ type: "text", text: String(content) }];
return [{ type: OPENAI_BLOCK.TEXT, text: String(content) }];
}
function safeParseJson(s) {
@@ -64,16 +66,16 @@ function convertMessages(messages = []) {
if (!m) continue;
const role = m.role;
if (role === "system") {
if (role === ROLE.SYSTEM) {
const t = flattenText(m.content);
if (t) systemTexts.push(t);
continue;
}
if (role === "tool") {
if (role === ROLE.TOOL) {
const value = typeof m.content === "string" ? m.content : flattenText(m.content);
out.push({
role: "tool",
role: ROLE.TOOL,
content: [{
type: "tool-result",
toolCallId: m.tool_call_id || "",
@@ -84,10 +86,10 @@ function convertMessages(messages = []) {
continue;
}
if (role === "assistant") {
if (role === ROLE.ASSISTANT) {
const blocks = [];
const text = flattenText(m.content);
if (text) blocks.push({ type: "text", text });
if (text) blocks.push({ type: OPENAI_BLOCK.TEXT, text });
if (Array.isArray(m.tool_calls)) {
for (const tc of m.tool_calls) {
const fn = tc.function || {};
@@ -99,11 +101,11 @@ function convertMessages(messages = []) {
});
}
}
out.push({ role: "assistant", content: blocks.length ? blocks : [{ type: "text", text: "" }] });
out.push({ role: ROLE.ASSISTANT, content: blocks.length ? blocks : [{ type: OPENAI_BLOCK.TEXT, text: "" }] });
continue;
}
out.push({ role: "user", content: toContentBlocks(m.content) });
out.push({ role: ROLE.USER, content: toContentBlocks(m.content) });
}
return { messages: out, system: systemTexts.join("\n\n") };
@@ -114,7 +116,7 @@ function convertTools(tools) {
const result = [];
for (const t of tools) {
if (!t) continue;
if (t.type === "function" && t.function) {
if (t.type === OPENAI_BLOCK.FUNCTION && t.function) {
result.push({
name: t.function.name,
description: t.function.description,
@@ -131,13 +133,13 @@ function convertTools(tools) {
return result.length ? result : undefined;
}
export function openaiToCommandCode(model, body, stream /* , credentials */) {
export function openaiToCommandCodeRequest(model, body, stream /* , credentials */) {
const { messages, system } = convertMessages(body.messages);
const params = {
model,
messages,
stream: stream !== false,
max_tokens: body.max_tokens ?? body.max_output_tokens ?? 64000,
max_tokens: body.max_tokens ?? body.max_output_tokens ?? DEFAULT_MAX_TOKENS,
temperature: body.temperature ?? 0.3,
};
@@ -167,4 +169,4 @@ export function openaiToCommandCode(model, body, stream /* , credentials */) {
};
}
register(FORMATS.OPENAI, FORMATS.COMMANDCODE, openaiToCommandCode, null);
register(FORMATS.OPENAI, FORMATS.COMMANDCODE, openaiToCommandCodeRequest, null);

View File

@@ -8,6 +8,8 @@
*/
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
import { DEFAULT_MIN_TOKENS } from "../../config/runtimeConfig.js";
function extractContent(content) {
if (typeof content === "string") return content;
@@ -15,7 +17,7 @@ function extractContent(content) {
return content
.filter(part => {
if (!part || typeof part !== "object") return false;
return part.type === "text" && typeof part.text === "string";
return part.type === OPENAI_BLOCK.TEXT && typeof part.text === "string";
})
.map(part => part.text || "")
.join("");
@@ -63,14 +65,14 @@ function convertMessages(messages) {
};
for (const msg of messages) {
if (msg.role === "assistant" && msg.tool_calls) {
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) {
for (const tc of msg.tool_calls) {
rememberToolMeta(tc.id || "", tc.function?.name || "tool");
}
}
if (msg.role === "assistant" && Array.isArray(msg.content)) {
if (msg.role === ROLE.ASSISTANT && Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part?.type !== "tool_use") continue;
if (part?.type !== CLAUDE_BLOCK.TOOL_USE) continue;
rememberToolMeta(part.id || "", part.name || "tool");
}
}
@@ -79,38 +81,38 @@ function convertMessages(messages) {
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (msg.role === "system") {
if (msg.role === ROLE.SYSTEM) {
result.push({
role: "user",
role: ROLE.USER,
content: `[System Instructions]\n${extractContent(msg.content)}`
});
continue;
}
if (msg.role === "tool") {
if (msg.role === ROLE.TOOL) {
const toolContent = extractContent(msg.content);
const toolCallId = msg.tool_call_id || "";
const toolMeta = toolCallMetaMap.get(toolCallId) || {};
const toolName = msg.name || toolMeta.name || "tool";
result.push({
role: "user",
role: ROLE.USER,
content: buildToolResultBlock(toolName, toolCallId, toolContent)
});
continue;
}
if (msg.role === "user" || msg.role === "assistant") {
if (msg.role === "user" && Array.isArray(msg.content)) {
if (msg.role === ROLE.USER || msg.role === ROLE.ASSISTANT) {
if (msg.role === ROLE.USER && Array.isArray(msg.content)) {
const parts = [];
for (const block of msg.content) {
if (!block || typeof block !== "object") continue;
if (block.type === "text") {
if (block.type === CLAUDE_BLOCK.TEXT) {
if (typeof block.text === "string") {
parts.push(block.text || "");
}
continue;
}
if (block.type === "tool_result") {
if (block.type === CLAUDE_BLOCK.TOOL_RESULT) {
const toolCallId = block.tool_use_id || "";
const toolMeta =
toolCallMetaMap.get(toolCallId) ||
@@ -121,25 +123,25 @@ function convertMessages(messages) {
}
}
const joined = parts.filter(Boolean).join("\n");
if (joined) result.push({ role: "user", content: joined });
if (joined) result.push({ role: ROLE.USER, content: joined });
continue;
}
const content = extractContent(msg.content);
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
const assistantMsg = { role: "assistant", content: content || "" };
if (msg.role === ROLE.ASSISTANT && msg.tool_calls && msg.tool_calls.length > 0) {
const assistantMsg = { role: ROLE.ASSISTANT, content: content || "" };
assistantMsg.tool_calls = msg.tool_calls.map(tc => {
const { index, ...rest } = tc || {};
return rest;
});
result.push(assistantMsg);
} else if (msg.role === "assistant" && Array.isArray(msg.content)) {
} else if (msg.role === ROLE.ASSISTANT && Array.isArray(msg.content)) {
const extractedToolCalls = msg.content
.filter(b => b?.type === "tool_use")
.filter(b => b?.type === CLAUDE_BLOCK.TOOL_USE)
.map(b => ({
id: b.id || "",
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
name: b.name || "tool",
arguments: JSON.stringify(b.input || {})
@@ -149,12 +151,12 @@ function convertMessages(messages) {
if (extractedToolCalls.length > 0) {
result.push({
role: "assistant",
role: ROLE.ASSISTANT,
content: content || "",
tool_calls: extractedToolCalls
});
} else if (content) {
result.push({ role: "assistant", content });
result.push({ role: ROLE.ASSISTANT, content });
}
} else {
if (content) {
@@ -167,7 +169,7 @@ function convertMessages(messages) {
return result;
}
export function buildCursorRequest(model, body, stream, credentials) {
export function openaiToCursorRequest(model, body, stream, credentials) {
const messages = convertMessages(body.messages || []);
// Strip fields irrelevant to Cursor (OpenAI/Anthropic-specific)
@@ -176,8 +178,8 @@ export function buildCursorRequest(model, body, stream, credentials) {
return {
...rest,
messages,
max_tokens: 32000
max_tokens: DEFAULT_MIN_TOKENS
};
}
register(FORMATS.OPENAI, FORMATS.CURSOR, buildCursorRequest, null);
register(FORMATS.OPENAI, FORMATS.CURSOR, openaiToCursorRequest, null);

View File

@@ -3,6 +3,7 @@ import { FORMATS } from "../formats.js";
import { DEFAULT_THINKING_AG_SIGNATURE, DEFAULT_THINKING_GEMINI_CLI_SIGNATURE } from "../../config/defaultThinkingSignature.js";
import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/appConstants.js";
import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js";
import { effortToThinkingLevel } from "../concerns/thinking.js";
function generateUUID() {
return crypto.randomUUID();
@@ -17,8 +18,9 @@ import {
generateSessionId,
generateProjectId,
cleanJSONSchemaForAntigravity
} from "../helpers/geminiHelper.js";
} from "../formats/gemini.js";
import { deriveSessionId } from "../../utils/sessionManager.js";
import { ROLE, GEMINI_ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
// Sanitize function names for Gemini API.
// Gemini requires: starts with [a-zA-Z_], followed by [a-zA-Z0-9_.:\-], max 64 chars.
@@ -62,9 +64,9 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
const tcID2Name = {};
if (body.messages && Array.isArray(body.messages)) {
for (const msg of body.messages) {
if (msg.role === "assistant" && msg.tool_calls) {
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) {
for (const tc of msg.tool_calls) {
if (tc.type === "function" && tc.id && tc.function?.name) {
if (tc.type === OPENAI_BLOCK.FUNCTION && tc.id && tc.function?.name) {
tcID2Name[tc.id] = tc.function.name;
}
}
@@ -76,7 +78,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
const toolResponses = {};
if (body.messages && Array.isArray(body.messages)) {
for (const msg of body.messages) {
if (msg.role === "tool" && msg.tool_call_id) {
if (msg.role === ROLE.TOOL && msg.tool_call_id) {
toolResponses[msg.tool_call_id] = msg.content;
}
}
@@ -89,17 +91,17 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
const role = msg.role;
const content = msg.content;
if (role === "system" && body.messages.length > 1) {
if (role === ROLE.SYSTEM && body.messages.length > 1) {
result.systemInstruction = {
role: "user",
role: GEMINI_ROLE.USER,
parts: [{ text: typeof content === "string" ? content : extractTextContent(content) }]
};
} else if (role === "user" || (role === "system" && body.messages.length === 1)) {
} else if (role === ROLE.USER || (role === ROLE.SYSTEM && body.messages.length === 1)) {
const parts = convertOpenAIContentToParts(content);
if (parts.length > 0) {
result.contents.push({ role: "user", parts });
result.contents.push({ role: GEMINI_ROLE.USER, parts });
}
} else if (role === "assistant") {
} else if (role === ROLE.ASSISTANT) {
const parts = [];
// Thinking/reasoning → thought part with signature
@@ -124,7 +126,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
const toolCallIds = [];
for (const tc of msg.tool_calls) {
if (tc.type !== "function") continue;
if (tc.type !== OPENAI_BLOCK.FUNCTION) continue;
const args = tryParseJSON(tc.function?.arguments || "{}");
parts.push({
@@ -139,7 +141,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
}
if (parts.length > 0) {
result.contents.push({ role: "model", parts });
result.contents.push({ role: GEMINI_ROLE.MODEL, parts });
}
// Check if there are actual tool responses in the next messages
@@ -177,11 +179,11 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
});
}
if (toolParts.length > 0) {
result.contents.push({ role: "user", parts: toolParts });
result.contents.push({ role: GEMINI_ROLE.USER, parts: toolParts });
}
}
} else if (parts.length > 0) {
result.contents.push({ role: "model", parts });
result.contents.push({ role: GEMINI_ROLE.MODEL, parts });
}
}
}
@@ -201,7 +203,7 @@ function openaiToGeminiBase(model, body, stream, signature = DEFAULT_THINKING_AG
});
}
// OpenAI format
else if (t.type === "function" && t.function) {
else if (t.type === OPENAI_BLOCK.FUNCTION && t.function) {
const fn = t.function;
const cleanedSchema = cleanJSONSchemaForAntigravity(structuredClone(fn.parameters || { type: "object", properties: {} }));
functionDeclarations.push({
@@ -235,8 +237,7 @@ export function openaiToGeminiCLIRequest(model, body, stream) {
// Accept both OpenAI chat (reasoning_effort) and Responses (reasoning.effort) shapes
const reasoningEffort = body.reasoning_effort ?? body.reasoning?.effort;
if (reasoningEffort) {
const effort = String(reasoningEffort).toLowerCase().trim();
const level = (effort === "none" || effort === "off") ? "minimal" : effort;
const level = effortToThinkingLevel(reasoningEffort);
gemini.generationConfig.thinkingConfig = { thinkingLevel: level, includeThoughts: level !== "minimal" };
}
@@ -298,7 +299,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
if (envelope.request.systemInstruction?.parts) {
envelope.request.systemInstruction.parts.unshift(...systemParts);
} else {
envelope.request.systemInstruction = { role: "user", parts: systemParts };
envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts };
}
// Add toolConfig for Antigravity
@@ -341,7 +342,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
for (const msg of claudeRequest.messages) {
if (Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "tool_use" && block.id && block.name) {
if (block.type === CLAUDE_BLOCK.TOOL_USE && block.id && block.name) {
toolUseIdToName[block.id] = block.name;
}
}
@@ -356,9 +357,9 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
if (Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "text") {
if (block.type === CLAUDE_BLOCK.TEXT) {
parts.push({ text: block.text });
} else if (block.type === "tool_use") {
} else if (block.type === CLAUDE_BLOCK.TOOL_USE) {
parts.push({
functionCall: {
id: block.id,
@@ -366,10 +367,10 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
args: block.input || {}
}
});
} else if (block.type === "tool_result") {
} else if (block.type === CLAUDE_BLOCK.TOOL_RESULT) {
let content = block.content;
if (Array.isArray(content)) {
content = content.map(c => c.type === "text" ? c.text : JSON.stringify(c)).join("\n");
content = content.map(c => c.type === CLAUDE_BLOCK.TEXT ? c.text : JSON.stringify(c)).join("\n");
}
// Resolve the original tool name from the id — Gemini requires it to match the functionCall name
const resolvedName = toolUseIdToName[block.tool_use_id]
@@ -390,7 +391,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
if (parts.length > 0) {
envelope.request.contents.push({
role: msg.role === "assistant" ? "model" : "user",
role: msg.role === ROLE.ASSISTANT ? GEMINI_ROLE.MODEL : GEMINI_ROLE.USER,
parts
});
}
@@ -439,7 +440,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
if (envelope.request.systemInstruction?.parts) {
envelope.request.systemInstruction.parts.unshift(...systemParts);
} else {
envelope.request.systemInstruction = { role: "user", parts: systemParts };
envelope.request.systemInstruction = { role: GEMINI_ROLE.USER, parts: systemParts };
}
return envelope;

View File

@@ -12,7 +12,9 @@ import {
KIRO_AGENTIC_SYSTEM_PROMPT,
resolveDefaultProfileArn
} from "../../config/kiroConstants.js";
import { parseDataUri } from "../helpers/imageHelper.js";
import { parseDataUri } from "../concerns/image.js";
import { DEFAULT_IMAGE_MIME } from "../schema/index.js";
import { ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
/** Render a single tool call as a readable text line. */
function toolCallToText(name, input) {
@@ -56,18 +58,18 @@ function flattenToolInteractions(messages) {
for (const msg of messages) {
// OpenAI tool-result message → user text line
if (msg.role === "tool") {
out.push({ role: "user", content: toolResultToText(msg.content) });
if (msg.role === ROLE.TOOL) {
out.push({ role: ROLE.USER, content: toolResultToText(msg.content) });
continue;
}
if (msg.role === "assistant") {
if (msg.role === ROLE.ASSISTANT) {
const parts = [];
if (Array.isArray(msg.content)) {
for (const c of msg.content) {
if (c.type === "tool_use") {
if (c.type === CLAUDE_BLOCK.TOOL_USE) {
parts.push(toolCallToText(c.name, c.input));
} else if (c.type === "text" || c.text) {
} else if (c.type === OPENAI_BLOCK.TEXT || c.text) {
parts.push(c.text || "");
}
}
@@ -77,15 +79,15 @@ function flattenToolInteractions(messages) {
for (const tc of msg.tool_calls || []) {
parts.push(toolCallToText(tc.function?.name, tc.function?.arguments));
}
out.push({ role: "assistant", content: parts.filter(Boolean).join("\n") });
out.push({ role: ROLE.ASSISTANT, content: parts.filter(Boolean).join("\n") });
continue;
}
// User messages: replace tool_result blocks with text, keep text + images.
if (msg.role === "user" && Array.isArray(msg.content)) {
if (msg.role === ROLE.USER && Array.isArray(msg.content)) {
const newContent = msg.content.map(c =>
c.type === "tool_result"
? { type: "text", text: toolResultToText(c.content) }
c.type === CLAUDE_BLOCK.TOOL_RESULT
? { type: OPENAI_BLOCK.TEXT, text: toolResultToText(c.content) }
: c
);
out.push({ ...msg, content: newContent });
@@ -267,8 +269,8 @@ function convertMessages(messages, tools, model) {
let role = msg.role;
// Normalize: system/tool -> user
if (role === "system" || role === "tool") {
role = "user";
if (role === ROLE.SYSTEM || role === ROLE.TOOL) {
role = ROLE.USER;
}
// If role changes, flush pending
@@ -277,7 +279,7 @@ function convertMessages(messages, tools, model) {
}
currentRole = role;
if (role === "user") {
if (role === ROLE.USER) {
// Extract content
let content = "";
if (typeof msg.content === "string") {
@@ -285,9 +287,9 @@ function convertMessages(messages, tools, model) {
} else if (Array.isArray(msg.content)) {
const textParts = [];
for (const c of msg.content) {
if (c.type === "text" || c.text) {
if (c.type === OPENAI_BLOCK.TEXT || c.text) {
textParts.push(c.text || "");
} else if (c.type === "image_url") {
} else if (c.type === OPENAI_BLOCK.IMAGE_URL) {
// OpenAI format: image_url.url with data URI
const url = c.image_url?.url || "";
const parsed = parseDataUri(url);
@@ -298,10 +300,10 @@ function convertMessages(messages, tools, model) {
// Kiro only supports base64 — fallback to URL text
textParts.push(`[Image: ${url}]`);
}
} else if (c.type === "image") {
} else if (c.type === CLAUDE_BLOCK.IMAGE) {
// Claude format: source.type = "base64", source.media_type, source.data
if (c.source?.type === "base64" && c.source?.data) {
const mediaType = c.source.media_type || "image/png";
const mediaType = c.source.media_type || DEFAULT_IMAGE_MIME;
const format = mediaType.split("/")[1] || mediaType;
pendingImages.push({ format, source: { bytes: c.source.data } });
}
@@ -310,7 +312,7 @@ function convertMessages(messages, tools, model) {
content = textParts.join("\n");
// Check for tool_result blocks
const toolResultBlocks = msg.content.filter(c => c.type === "tool_result");
const toolResultBlocks = msg.content.filter(c => c.type === CLAUDE_BLOCK.TOOL_RESULT);
if (toolResultBlocks.length > 0) {
toolResultBlocks.forEach(block => {
const text = Array.isArray(block.content)
@@ -327,7 +329,7 @@ function convertMessages(messages, tools, model) {
}
// Handle tool role (from normalized)
if (msg.role === "tool") {
if (msg.role === ROLE.TOOL) {
const toolContent = typeof msg.content === "string" ? msg.content : "";
pendingToolResults.push({
toolUseId: msg.tool_call_id,
@@ -337,16 +339,16 @@ function convertMessages(messages, tools, model) {
} else if (content) {
pendingUserContent.push(content);
}
} else if (role === "assistant") {
} else if (role === ROLE.ASSISTANT) {
// Extract text content and tool uses
let textContent = "";
let toolUses = [];
if (Array.isArray(msg.content)) {
const textBlocks = msg.content.filter(c => c.type === "text");
const textBlocks = msg.content.filter(c => c.type === OPENAI_BLOCK.TEXT);
textContent = textBlocks.map(b => b.text).join("\n").trim();
const toolUseBlocks = msg.content.filter(c => c.type === "tool_use");
const toolUseBlocks = msg.content.filter(c => c.type === CLAUDE_BLOCK.TOOL_USE);
toolUses = toolUseBlocks;
} else if (typeof msg.content === "string") {
textContent = msg.content.trim();
@@ -509,7 +511,7 @@ function convertMessages(messages, tools, model) {
* `thinking`, OpenAI `reasoning_effort`, AMP/Cursor magic tags, and model
* name hints.
*/
export function buildKiroPayload(model, body, stream, credentials) {
export function openaiToKiroRequest(model, body, stream, credentials) {
const messages = body.messages || [];
const tools = body.tools || [];
const maxTokens = 32000;
@@ -582,4 +584,4 @@ export function buildKiroPayload(model, body, stream, credentials) {
return payload;
}
register(FORMATS.OPENAI, FORMATS.KIRO, buildKiroPayload, null);
register(FORMATS.OPENAI, FORMATS.KIRO, openaiToKiroRequest, null);

View File

@@ -1,278 +0,0 @@
/**
* OpenAI to Kiro Request Translator
* Converts OpenAI Chat Completions format to Kiro/AWS CodeWhisperer format
*/
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { v4 as uuidv4 } from "uuid";
/**
* Convert OpenAI messages to Kiro format
*/
function convertMessages(messages, tools, model) {
let history = [];
let currentMessage = null;
let systemPrompt = "";
const toolResultsMap = new Map();
for (const msg of messages) {
if (msg.role === "tool" && msg.tool_call_id) {
const content = typeof msg.content === "string" ? msg.content :
(Array.isArray(msg.content) ? msg.content.map(c => c.text || "").join("\n") : "");
toolResultsMap.set(msg.tool_call_id, content);
}
if (msg.role === "user" && Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "tool_result" && block.tool_use_id) {
const content = Array.isArray(block.content)
? block.content.map(c => c.text || "").join("\n")
: (typeof block.content === "string" ? block.content : "");
toolResultsMap.set(block.tool_use_id, content);
}
}
}
}
for (const msg of messages) {
const role = msg.role;
if (role === "tool") continue;
const content = typeof msg.content === "string" ? msg.content :
(Array.isArray(msg.content) ? msg.content.map(c => c.text || "").join("\n") : "");
if (role === "system") {
systemPrompt += (systemPrompt ? "\n" : "") + content;
continue;
}
if (role === "user") {
let finalContent = content;
let toolResults = [];
// Check if this user message contains tool_result blocks
if (Array.isArray(msg.content)) {
const toolResultBlocks = msg.content.filter(c => c.type === "tool_result");
if (toolResultBlocks.length > 0) {
toolResults = toolResultBlocks.map(block => {
const text = Array.isArray(block.content)
? block.content.map(c => c.text || "").join("\n")
: (typeof block.content === "string" ? block.content : "");
return {
toolUseId: block.tool_use_id,
status: "success",
content: [{ text: text }]
};
});
// Set simple content when tool results exist
finalContent = content || "Continue";
}
}
const userMsg = {
userInputMessage: {
content: finalContent,
modelId: "",
}
};
// Add tool results to userInputMessageContext
if (toolResults.length > 0) {
if (!userMsg.userInputMessage.userInputMessageContext) {
userMsg.userInputMessage.userInputMessageContext = {};
}
userMsg.userInputMessage.userInputMessageContext.toolResults = toolResults;
}
// Add tools to first user message
if (tools && tools.length > 0 && history.length === 0) {
if (!userMsg.userInputMessage.userInputMessageContext) {
userMsg.userInputMessage.userInputMessageContext = {};
}
userMsg.userInputMessage.userInputMessageContext.tools = tools.map(t => {
const name = t.function?.name || t.name;
let description = t.function?.description || t.description || "";
if (!description.trim()) {
description = `Tool: ${name}`;
}
return {
toolSpecification: {
name,
description,
inputSchema: {
json: t.function?.parameters || t.parameters || t.input_schema || {}
}
}
};
});
}
currentMessage = userMsg;
history.push(userMsg);
}
if (role === "assistant") {
// Extract text content and tool uses separately from content array
let textContent = "";
let toolUses = [];
if (Array.isArray(msg.content)) {
const textBlocks = msg.content.filter(c => c.type === "text");
textContent = textBlocks.map(b => b.text).join("\n").trim();
const toolUseBlocks = msg.content.filter(c => c.type === "tool_use");
toolUses = toolUseBlocks;
} else if (typeof msg.content === "string") {
textContent = msg.content.trim();
}
// Fallback for OpenAI tool_calls format
if (msg.tool_calls && msg.tool_calls.length > 0) {
toolUses = msg.tool_calls;
}
const assistantMsg = {
assistantResponseMessage: {
content: textContent || "Call tools"
}
};
if (toolUses.length > 0) {
assistantMsg.assistantResponseMessage.toolUses = toolUses.map(tc => {
if (tc.function) {
// OpenAI format
return {
toolUseId: tc.id || uuidv4(),
name: tc.function.name,
input: typeof tc.function.arguments === "string"
? JSON.parse(tc.function.arguments)
: (tc.function.arguments || {})
};
} else {
// Anthropic format
return {
toolUseId: tc.id || uuidv4(),
name: tc.name,
input: tc.input || {}
};
}
});
}
history.push(assistantMsg);
}
}
// If last message in history is userInputMessage, use it as currentMessage
if (history.length > 0 && history[history.length - 1].userInputMessage) {
currentMessage = history.pop();
}
const firstHistoryItem = history[0];
if (firstHistoryItem?.userInputMessage?.userInputMessageContext?.tools &&
!currentMessage?.userInputMessage?.userInputMessageContext?.tools) {
if (!currentMessage.userInputMessage.userInputMessageContext) {
currentMessage.userInputMessage.userInputMessageContext = {};
}
currentMessage.userInputMessage.userInputMessageContext.tools =
firstHistoryItem.userInputMessage.userInputMessageContext.tools;
}
// Clean up history for Kiro API compatibility
history.forEach(item => {
if (item.userInputMessage?.userInputMessageContext?.tools) {
delete item.userInputMessage.userInputMessageContext.tools;
}
if (item.userInputMessage?.userInputMessageContext &&
Object.keys(item.userInputMessage.userInputMessageContext).length === 0) {
delete item.userInputMessage.userInputMessageContext;
}
if (item.userInputMessage && !item.userInputMessage.modelId) {
item.userInputMessage.modelId = model;
}
});
// Merge consecutive user messages (Kiro requires alternating user/assistant)
const mergedHistory = [];
for (let i = 0; i < history.length; i++) {
const current = history[i];
if (current.userInputMessage &&
mergedHistory.length > 0 &&
mergedHistory[mergedHistory.length - 1].userInputMessage) {
const prev = mergedHistory[mergedHistory.length - 1];
prev.userInputMessage.content += "\n\n" + current.userInputMessage.content;
} else {
mergedHistory.push(current);
}
}
history = mergedHistory;
return { history, currentMessage, systemPrompt };
}
/**
* Build Kiro payload from OpenAI format
*/
function buildKiroPayload(model, body, stream, credentials) {
const messages = body.messages || [];
const tools = body.tools || [];
const maxTokens = 32000;
const temperature = body.temperature;
const topP = body.top_p;
const { history, currentMessage, systemPrompt } = convertMessages(messages, tools, model);
const profileArn = credentials?.providerSpecificData?.profileArn || "";
let finalContent = currentMessage?.userInputMessage?.content || "";
if (systemPrompt) {
finalContent = `[System: ${systemPrompt}]\n\n${finalContent}`;
}
const timestamp = new Date().toISOString();
finalContent = `[Context: Current time is ${timestamp}]\n\n${finalContent}`;
const payload = {
conversationState: {
chatTriggerType: "MANUAL",
conversationId: uuidv4(),
currentMessage: {
userInputMessage: {
content: finalContent,
modelId: model,
origin: "AI_EDITOR",
...(currentMessage?.userInputMessage?.userInputMessageContext && {
userInputMessageContext: currentMessage.userInputMessage.userInputMessageContext
})
}
},
history: history
}
};
if (profileArn) {
payload.profileArn = profileArn;
}
if (maxTokens || temperature !== undefined || topP !== undefined) {
payload.inferenceConfig = {};
if (maxTokens) payload.inferenceConfig.maxTokens = maxTokens;
if (temperature !== undefined) payload.inferenceConfig.temperature = temperature;
if (topP !== undefined) payload.inferenceConfig.topP = topP;
}
return payload;
}
register(FORMATS.OPENAI, FORMATS.KIRO, buildKiroPayload, null);
export { buildKiroPayload };

View File

@@ -1,6 +1,8 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { parseDataUri } from "../helpers/imageHelper.js";
import { parseDataUri } from "../concerns/image.js";
import { safeParseJSON } from "../concerns/json.js";
import { ROLE, OPENAI_BLOCK } from "../schema/index.js";
/**
* Convert OpenAI request to Ollama format
@@ -68,7 +70,7 @@ function normalizeMessages(messages) {
// First pass: build tool_call_id -> tool_name map from assistant messages
for (const msg of messages) {
if (msg.role === "assistant" && msg.tool_calls) {
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) {
for (const tc of msg.tool_calls) {
if (tc.id && tc.function?.name) {
toolCallMap.set(tc.id, tc.function.name);
@@ -80,7 +82,7 @@ function normalizeMessages(messages) {
// Second pass: convert messages
for (const msg of messages) {
// Handle tool result messages (OpenAI format -> Ollama format)
if (msg.role === "tool") {
if (msg.role === ROLE.TOOL) {
const toolResult = normalizeContent(msg.content);
if (!toolResult) continue;
@@ -88,7 +90,7 @@ function normalizeMessages(messages) {
const toolName = toolCallMap.get(msg.tool_call_id) || msg.name || "unknown_tool";
result.push({
role: "tool",
role: ROLE.TOOL,
tool_name: toolName,
content: toolResult
});
@@ -96,23 +98,23 @@ function normalizeMessages(messages) {
}
// Handle assistant messages with tool_calls
if (msg.role === "assistant" && msg.tool_calls) {
if (msg.role === ROLE.ASSISTANT && msg.tool_calls) {
const content = normalizeContent(msg.content) || "";
// Convert OpenAI tool_calls format to Ollama format
const ollamaToolCalls = msg.tool_calls.map(tc => ({
type: "function",
type: OPENAI_BLOCK.FUNCTION,
function: {
index: tc.index || 0,
name: tc.function?.name || "",
arguments: typeof tc.function?.arguments === "string"
? JSON.parse(tc.function.arguments || "{}")
? safeParseJSON(tc.function.arguments || "{}", {})
: tc.function?.arguments || {}
}
}));
result.push({
role: "assistant",
role: ROLE.ASSISTANT,
content: content,
tool_calls: ollamaToolCalls
});
@@ -125,7 +127,7 @@ function normalizeMessages(messages) {
const images = extractImagesFromContent(msg.content);
// Skip empty messages (except assistant)
if (!content && role !== "assistant") continue;
if (!content && role !== ROLE.ASSISTANT) continue;
const out = {
role: role,
@@ -154,7 +156,7 @@ function normalizeContent(content) {
if (Array.isArray(content)) {
// Extract text from content array
const textParts = content
.filter(block => block && block.type === "text" && block.text)
.filter(block => block && block.type === OPENAI_BLOCK.TEXT && block.text)
.map(block => block.text);
return textParts.join("\n") || "";
@@ -175,7 +177,7 @@ function extractImagesFromContent(content) {
const images = [];
for (const block of content) {
if (!block || block.type !== "image_url") continue;
if (!block || block.type !== OPENAI_BLOCK.IMAGE_URL) continue;
const url = typeof block.image_url === "string" ? block.image_url : block.image_url?.url;
if (typeof url !== "string" || !url) continue;