Files
9router/open-sse/translator/request/antigravity-to-openai.js
T
Sahrul Ramadhan HardiansyahandCursor ce6120ce7b fix(translator): strict Anthropic content block compliance (#2225)
Filter empty text blocks from thoughtSignature-only parts, preserve
tool_calls when functionResponse and functionCall coexist in the same
content, and skip empty regular text parts before they reach Claude.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 10:58:39 +07:00

243 lines
7.1 KiB
JavaScript

import { register } from "../index.js";
import { FORMATS } from "../formats.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 } }
export function antigravityToOpenAIRequest(model, body, stream) {
const req = body.request || body;
const result = {
model: model,
messages: [],
stream: stream
};
// Generation config
if (req.generationConfig) {
const config = req.generationConfig;
if (config.maxOutputTokens) {
const tempBody = { max_tokens: config.maxOutputTokens, tools: req.tools };
result.max_tokens = adjustMaxTokens(tempBody);
}
if (config.temperature !== undefined) {
result.temperature = config.temperature;
}
if (config.topP !== undefined) {
result.top_p = config.topP;
}
if (config.topK !== undefined) {
result.top_k = config.topK;
}
// Thinking config → reasoning_effort
if (config.thinkingConfig) {
const effort = budgetToEffort(config.thinkingConfig.thinkingBudget || 0);
if (effort) result.reasoning_effort = effort;
}
}
// System instruction
if (req.systemInstruction) {
const systemText = extractText(req.systemInstruction);
if (systemText) {
result.messages.push({ role: ROLE.SYSTEM, content: systemText });
}
}
// Convert contents to messages
if (req.contents && Array.isArray(req.contents)) {
for (const content of req.contents) {
const converted = convertContent(content);
if (converted) {
if (Array.isArray(converted)) {
result.messages.push(...converted);
} else {
result.messages.push(converted);
}
}
}
}
// Tools
if (req.tools && Array.isArray(req.tools)) {
result.tools = [];
for (const tool of req.tools) {
if (tool.functionDeclarations) {
for (const func of tool.functionDeclarations) {
result.tools.push({
type: OPENAI_BLOCK.FUNCTION,
function: {
name: func.name,
description: func.description || "",
parameters: normalizeSchemaTypes(func.parameters) || { type: "object", properties: {} }
}
});
}
}
}
}
return result;
}
// Recursively convert Antigravity schema types (OBJECT, STRING, etc.) to lowercase
// and strip unsupported fields like enumDescriptions
function normalizeSchemaTypes(schema) {
if (!schema || typeof schema !== "object") return schema;
const result = Array.isArray(schema) ? [...schema] : { ...schema };
if (typeof result.type === "string") {
result.type = result.type.toLowerCase();
}
// Strip enumDescriptions — not supported by upstream APIs
delete result.enumDescriptions;
if (result.properties) {
const normalized = {};
for (const [key, val] of Object.entries(result.properties)) {
normalized[key] = normalizeSchemaTypes(val);
}
result.properties = normalized;
}
if (result.items) {
result.items = normalizeSchemaTypes(result.items);
}
return result;
}
// Convert Antigravity content to OpenAI message
// Handles: text, thought, thoughtSignature, functionCall, functionResponse, inlineData
function convertContent(content) {
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;
}
const textParts = [];
const toolCalls = [];
const toolResults = [];
let reasoningContent = "";
for (const part of content.parts) {
// Thinking content (thought: true)
if (part.thought === true && part.text) {
reasoningContent += part.text;
continue;
}
// Text with thoughtSignature = regular text after thinking
if (part.thoughtSignature && part.text !== undefined) {
if (part.text) {
textParts.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
}
continue;
}
// Regular text
if (part.text !== undefined && part.text !== "") {
textParts.push({ type: OPENAI_BLOCK.TEXT, text: part.text });
}
// Inline data (images)
if (part.inlineData) {
textParts.push({
type: OPENAI_BLOCK.IMAGE_URL,
image_url: {
url: encodeDataUri(part.inlineData.mimeType, part.inlineData.data)
}
});
}
// Function call
if (part.functionCall) {
toolCalls.push({
// Deterministic id from name so the matching functionResponse pairs correctly.
id: part.functionCall.id || `call_${part.functionCall.name}`,
type: OPENAI_BLOCK.FUNCTION,
function: {
name: part.functionCall.name,
arguments: JSON.stringify(part.functionCall.args || {})
}
});
}
// Function response → collect all, each becomes a separate tool message
if (part.functionResponse) {
toolResults.push({
role: ROLE.TOOL,
tool_call_id: part.functionResponse.id || `call_${part.functionResponse.name}`,
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
});
}
}
// Content with functionResponses — return array of tool result messages,
// plus an assistant message for any co-located tool calls / text.
if (toolResults.length > 0) {
if (toolCalls.length > 0 || textParts.length > 0 || reasoningContent) {
const assistantMsg = { role: ROLE.ASSISTANT };
if (textParts.length > 0) {
assistantMsg.content = collapseTextParts(textParts);
}
if (reasoningContent) {
assistantMsg.reasoning_content = reasoningContent;
}
if (toolCalls.length > 0) {
assistantMsg.tool_calls = toolCalls;
}
return [...toolResults, assistantMsg];
}
return toolResults;
}
// Assistant with tool calls
if (toolCalls.length > 0) {
const msg = { role: ROLE.ASSISTANT };
if (textParts.length > 0) {
msg.content = collapseTextParts(textParts);
}
if (reasoningContent) {
msg.reasoning_content = reasoningContent;
}
msg.tool_calls = toolCalls;
return msg;
}
// Regular message
if (textParts.length > 0 || reasoningContent) {
const msg = { role };
if (textParts.length > 0) {
msg.content = collapseTextParts(textParts);
}
if (reasoningContent) {
msg.reasoning_content = reasoningContent;
}
return msg;
}
return null;
}
// Extract text from systemInstruction
function extractText(instruction) {
if (typeof instruction === "string") return instruction;
if (instruction.parts && Array.isArray(instruction.parts)) {
return instruction.parts.map(p => p.text || "").join("");
}
return "";
}
// Register
register(FORMATS.ANTIGRAVITY, FORMATS.OPENAI, antigravityToOpenAIRequest, null);