feat(antigravity): integrate Antigravity tool with MITM support and update CLI tools

This commit is contained in:
decolua
2026-02-08 16:28:13 +07:00
parent 18712b24cf
commit 2e854bd4c9
21 changed files with 1680 additions and 20 deletions

View File

@@ -32,6 +32,7 @@ function ensureInitialized() {
require("./request/openai-to-claude.js");
require("./request/gemini-to-openai.js");
require("./request/openai-to-gemini.js");
require("./request/antigravity-to-openai.js");
require("./request/openai-responses.js");
require("./request/openai-to-kiro.js");
require("./request/openai-to-cursor.js");
@@ -40,6 +41,7 @@ function ensureInitialized() {
require("./response/claude-to-openai.js");
require("./response/openai-to-claude.js");
require("./response/gemini-to-openai.js");
require("./response/openai-to-antigravity.js");
require("./response/openai-responses.js");
require("./response/kiro-to-openai.js");
require("./response/cursor-to-openai.js");

View File

@@ -0,0 +1,223 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { adjustMaxTokens } from "../helpers/maxTokensHelper.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 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";
}
}
}
}
// System instruction
if (req.systemInstruction) {
const systemText = extractText(req.systemInstruction);
if (systemText) {
result.messages.push({ 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: "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
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();
}
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 === "model" ? "assistant" : content.role === "user" ? "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) {
textParts.push({ type: "text", text: part.text });
continue;
}
// Regular text
if (part.text !== undefined) {
textParts.push({ type: "text", text: part.text });
}
// Inline data (images)
if (part.inlineData) {
textParts.push({
type: "image_url",
image_url: {
url: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
}
});
}
// Function call
if (part.functionCall) {
toolCalls.push({
id: part.functionCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
type: "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: "tool",
tool_call_id: part.functionResponse.id || part.functionResponse.name,
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
});
}
}
// Content with only functionResponses → return array of tool messages
if (toolResults.length > 0) {
return toolResults;
}
// Assistant with tool calls
if (toolCalls.length > 0) {
const msg = { role: "assistant" };
if (textParts.length > 0) {
msg.content = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : 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 = textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : 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);

View File

@@ -86,6 +86,18 @@ function openaiToGeminiBase(model, body, stream) {
} else if (role === "assistant") {
const parts = [];
// Thinking/reasoning → thought part with signature
if (msg.reasoning_content) {
parts.push({
thought: true,
text: msg.reasoning_content
});
parts.push({
thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE,
text: ""
});
}
if (content) {
const text = typeof content === "string" ? content : extractTextContent(content);
if (text) {

View File

@@ -0,0 +1,120 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
// Convert OpenAI SSE chunk to Antigravity SSE format
// Real Antigravity format:
// data: {"response":{"candidates":[{"content":{"role":"model","parts":[...]}, "finishReason":"STOP"}], "usageMetadata":{...}, "modelVersion":"...", "responseId":"..."}}
// Tool calls: OpenAI sends incremental args across chunks → accumulate and emit ONCE at finish
export function openaiToAntigravityResponse(chunk, state) {
if (!chunk) return null;
const choice = chunk.choices?.[0];
if (!choice) {
if (chunk.usage) {
state._usage = chunk.usage;
}
return null;
}
const delta = choice.delta || {};
const finishReason = choice.finish_reason;
// Init state
if (!state._toolCallAccum) state._toolCallAccum = {};
if (!state._responseId) state._responseId = chunk.id || `resp_${Date.now()}`;
if (!state._modelVersion) state._modelVersion = chunk.model || "";
const parts = [];
// Thinking/reasoning → thought part
if (delta.reasoning_content) {
parts.push({ thought: true, text: delta.reasoning_content });
}
// Text content
if (delta.content) {
parts.push({ text: delta.content });
}
// Accumulate tool calls silently (no emit until finish)
if (delta.tool_calls) {
for (const tc of delta.tool_calls) {
const idx = tc.index ?? 0;
if (!state._toolCallAccum[idx]) {
state._toolCallAccum[idx] = { id: "", name: "", arguments: "" };
}
const accum = state._toolCallAccum[idx];
if (tc.id) accum.id = tc.id;
if (tc.function?.name) accum.name += tc.function.name;
if (tc.function?.arguments) accum.arguments += tc.function.arguments;
}
// Skip emit — wait for finish_reason
if (parts.length === 0 && !finishReason) return null;
}
// On finish, emit accumulated tool calls as complete functionCall parts
if (finishReason) {
const indices = Object.keys(state._toolCallAccum);
for (const idx of indices) {
const accum = state._toolCallAccum[idx];
let args = {};
try { args = JSON.parse(accum.arguments); } catch { /* empty */ }
parts.push({
functionCall: {
name: accum.name,
args
}
});
}
}
// Skip empty non-finish chunks
if (parts.length === 0 && !finishReason) return null;
// Ensure at least empty text part on finish with no content
if (parts.length === 0 && finishReason) {
parts.push({ text: "" });
}
// Build candidate
const candidate = { content: { role: "model", parts } };
// Finish reason mapping
if (finishReason) {
const reasonMap = {
"stop": "STOP",
"length": "MAX_TOKENS",
"tool_calls": "STOP",
"content_filter": "SAFETY"
};
candidate.finishReason = reasonMap[finishReason] || "STOP";
}
// Build response
const response = {
candidates: [candidate],
modelVersion: state._modelVersion,
responseId: state._responseId
};
// Usage metadata
const usage = chunk.usage || state._usage;
if (usage) {
response.usageMetadata = {
promptTokenCount: usage.prompt_tokens || 0,
candidatesTokenCount: usage.completion_tokens || 0,
totalTokenCount: usage.total_tokens || 0
};
if (usage.completion_tokens_details?.reasoning_tokens) {
response.usageMetadata.thoughtsTokenCount = usage.completion_tokens_details.reasoning_tokens;
}
if (usage.prompt_tokens_details?.cached_tokens) {
response.usageMetadata.cachedContentTokenCount = usage.prompt_tokens_details.cached_tokens;
}
}
return { response };
}
// Register
register(FORMATS.OPENAI, FORMATS.ANTIGRAVITY, null, openaiToAntigravityResponse);