Fix Antigravity

This commit is contained in:
decolua
2026-01-12 16:53:27 +07:00
parent 87c8f7f229
commit fb5be37e14
5 changed files with 320 additions and 52 deletions

2
.gitignore vendored
View File

@@ -56,6 +56,6 @@ cursor/*
PUBLIC.md
scripts/*
Thanks.md
package.json
PUBLIC.en.md
PR/*
package-lock.json

6
CHANGELOG.md Normal file
View File

@@ -0,0 +1,6 @@
# v0.2.21 (2026-01-12)
## Changes
- Update ReadMe
- Fix bug **antigravity**

View File

@@ -2,6 +2,8 @@ import crypto from "crypto";
import { BaseExecutor } from "./base.js";
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
const MAX_RETRY_AFTER_MS = 5000;
export class AntigravityExecutor extends BaseExecutor {
constructor() {
super("antigravity", PROVIDERS.antigravity);
@@ -26,6 +28,15 @@ export class AntigravityExecutor extends BaseExecutor {
transformRequest(model, body, stream, credentials) {
const projectId = credentials?.projectId || this.generateProjectId();
const transformedRequest = {
...body.request,
sessionId: body.request?.sessionId || this.generateSessionId(),
safetySettings: undefined,
toolConfig: body.request?.tools?.length > 0
? { functionCallingConfig: { mode: "VALIDATED" } }
: body.request?.toolConfig
};
return {
...body,
project: projectId,
@@ -33,14 +44,7 @@ export class AntigravityExecutor extends BaseExecutor {
userAgent: "antigravity",
requestType: "agent",
requestId: `agent-${crypto.randomUUID()}`,
request: {
...body.request,
sessionId: body.request?.sessionId || this.generateSessionId(),
safetySettings: undefined,
toolConfig: body.request?.tools?.length > 0
? { functionCallingConfig: { mode: "VALIDATED" } }
: body.request?.toolConfig
}
request: transformedRequest
};
}
@@ -85,6 +89,105 @@ export class AntigravityExecutor extends BaseExecutor {
generateSessionId() {
return `-${Math.floor(Math.random() * 9_000_000_000_000_000_000)}`;
}
parseRetryHeaders(headers) {
console.log("🚀 ~ AntigravityExecutor ~ parseRetryHeaders ~ headers:", headers)
if (!headers?.get) return null;
const retryAfter = headers.get('retry-after');
if (retryAfter) {
const seconds = parseInt(retryAfter, 10);
if (!isNaN(seconds) && seconds > 0) return seconds * 1000;
const date = new Date(retryAfter);
if (!isNaN(date.getTime())) {
const diff = date.getTime() - Date.now();
return diff > 0 ? diff : null;
}
}
const resetAfter = headers.get('x-ratelimit-reset-after');
if (resetAfter) {
const seconds = parseInt(resetAfter, 10);
if (!isNaN(seconds) && seconds > 0) return seconds * 1000;
}
const resetTimestamp = headers.get('x-ratelimit-reset');
if (resetTimestamp) {
const ts = parseInt(resetTimestamp, 10) * 1000;
const diff = ts - Date.now();
return diff > 0 ? diff : null;
}
return null;
}
async execute({ model, body, stream, credentials, signal, log }) {
const fallbackCount = this.getFallbackCount();
let lastError = null;
let lastStatus = 0;
const MAX_AUTO_RETRIES = 2;
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
const url = this.buildUrl(model, stream, urlIndex);
const headers = this.buildHeaders(credentials, stream);
const transformedBody = this.transformRequest(model, body, stream, credentials);
let retryAttempts = 0;
try {
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(transformedBody),
signal
});
if (response.status === 429 || response.status === 503) {
const retryMs = this.parseRetryHeaders(response.headers);
if (retryMs && retryMs <= MAX_RETRY_AFTER_MS) {
log?.debug?.("RETRY", `${response.status} with Retry-After: ${Math.ceil(retryMs/1000)}s, waiting...`);
await new Promise(resolve => setTimeout(resolve, retryMs));
urlIndex--;
continue;
}
// Auto retry only for 429 when retryMs is 0 or undefined
if (response.status === 429 && (!retryMs || retryMs === 0) && retryAttempts < MAX_AUTO_RETRIES) {
retryAttempts++;
log?.debug?.("RETRY", `429 auto retry ${retryAttempts}/${MAX_AUTO_RETRIES} after 1s`);
await new Promise(resolve => setTimeout(resolve, 1000));
urlIndex--;
continue;
}
log?.debug?.("RETRY", `${response.status}, Retry-After ${retryMs ? `too long (${Math.ceil(retryMs/1000)}s)` : 'missing'}, trying fallback`);
lastStatus = response.status;
if (urlIndex + 1 < fallbackCount) {
continue;
}
}
if (this.shouldRetry(response.status, urlIndex)) {
log?.debug?.("RETRY", `${response.status} on ${url}, trying fallback ${urlIndex + 1}`);
lastStatus = response.status;
continue;
}
return { response, url, headers, transformedBody };
} catch (error) {
lastError = error;
if (urlIndex + 1 < fallbackCount) {
log?.debug?.("RETRY", `Error on ${url}, trying fallback ${urlIndex + 1}`);
continue;
}
throw error;
}
}
throw lastError || new Error(`All ${fallbackCount} URLs failed with status ${lastStatus}`);
}
}
export default AntigravityExecutor;

View File

@@ -252,6 +252,37 @@ function tryParseJSON(str) {
}
}
// OpenAI -> Claude format for Antigravity (without system prompt modifications)
function openaiToClaudeRequestForAntigravity(model, body, stream) {
const result = openaiToClaudeRequest(model, body, stream);
// Remove Claude Code system prompt, keep only user's system messages
if (result.system && Array.isArray(result.system)) {
result.system = result.system.filter(block =>
!block.text || !block.text.includes("You are Claude Code")
);
if (result.system.length === 0) {
delete result.system;
}
}
// Un-capitalize tool names (Antigravity doesn't require capitalized names)
if (result.tools && Array.isArray(result.tools)) {
result.tools = result.tools.map(tool => {
if (tool.name) {
const originalName = tool.name.charAt(0).toLowerCase() + tool.name.slice(1);
return { ...tool, name: originalName };
}
return tool;
});
}
return result;
}
// Export for use in other translators
export { openaiToClaudeRequestForAntigravity };
// Register
register(FORMATS.OPENAI, FORMATS.CLAUDE, openaiToClaudeRequest, null);

View File

@@ -2,6 +2,7 @@ import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.js";
import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/constants.js";
import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js";
function generateUUID() {
return crypto.randomUUID();
@@ -84,7 +85,7 @@ function openaiToGeminiBase(model, body, stream) {
}
} else if (role === "assistant") {
const parts = [];
if (content) {
const text = typeof content === "string" ? content : extractTextContent(content);
if (text) {
@@ -96,7 +97,7 @@ function openaiToGeminiBase(model, body, stream) {
const toolCallIds = [];
for (const tc of msg.tool_calls) {
if (tc.type !== "function") continue;
const args = tryParseJSON(tc.function?.arguments || "{}");
parts.push({
thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE,
@@ -113,37 +114,43 @@ function openaiToGeminiBase(model, body, stream) {
result.contents.push({ role: "model", parts });
}
// Append function responses
const toolParts = [];
for (const fid of toolCallIds) {
let name = tcID2Name[fid];
if (!name) {
const idParts = fid.split("-");
if (idParts.length > 2) {
name = idParts.slice(0, -2).join("-");
} else {
name = fid;
// Check if there are actual tool responses in the next messages
const hasActualResponses = toolCallIds.some(fid => toolResponses[fid]);
if (hasActualResponses) {
const toolParts = [];
for (const fid of toolCallIds) {
if (!toolResponses[fid]) continue;
let name = tcID2Name[fid];
if (!name) {
const idParts = fid.split("-");
if (idParts.length > 2) {
name = idParts.slice(0, -2).join("-");
} else {
name = fid;
}
}
}
let resp = toolResponses[fid] || "{}";
let parsedResp = tryParseJSON(resp);
if (parsedResp === null) {
parsedResp = { result: resp };
} else if (typeof parsedResp !== "object") {
parsedResp = { result: parsedResp };
}
toolParts.push({
functionResponse: {
id: fid,
name: name,
response: { result: parsedResp }
let resp = toolResponses[fid];
let parsedResp = tryParseJSON(resp);
if (parsedResp === null) {
parsedResp = { result: resp };
} else if (typeof parsedResp !== "object") {
parsedResp = { result: parsedResp };
}
});
}
if (toolParts.length > 0) {
result.contents.push({ role: "user", parts: toolParts });
toolParts.push({
functionResponse: {
id: fid,
name: name,
response: { result: parsedResp }
}
});
}
if (toolParts.length > 0) {
result.contents.push({ role: "user", parts: toolParts });
}
}
} else if (parts.length > 0) {
result.contents.push({ role: "model", parts });
@@ -156,7 +163,16 @@ function openaiToGeminiBase(model, body, stream) {
if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) {
const functionDeclarations = [];
for (const t of body.tools) {
if (t.type === "function" && t.function) {
// Check if already in Anthropic/Claude format (no type field, direct name/description/input_schema)
if (t.name && t.input_schema) {
functionDeclarations.push({
name: t.name,
description: t.description || "",
parameters: t.input_schema || { type: "object", properties: {} }
});
}
// OpenAI format
else if (t.type === "function" && t.function) {
const fn = t.function;
functionDeclarations.push({
name: fn.name,
@@ -165,7 +181,7 @@ function openaiToGeminiBase(model, body, stream) {
});
}
}
if (functionDeclarations.length > 0) {
result.tools = [{ functionDeclarations }];
}
@@ -183,7 +199,7 @@ function openaiToGeminiRequest(model, body, stream) {
function openaiToGeminiCLIRequest(model, body, stream) {
const gemini = openaiToGeminiBase(model, body, stream);
const isClaude = model.toLowerCase().includes("claude");
// Add thinking config for CLI
if (body.reasoning_effort) {
const budgetMap = { low: 1024, medium: 8192, high: 32768 };
@@ -207,12 +223,13 @@ function openaiToGeminiCLIRequest(model, body, stream) {
for (const fn of gemini.tools[0].functionDeclarations) {
if (fn.parameters) {
const cleanedSchema = cleanJSONSchemaForAntigravity(fn.parameters);
if (isClaude) {
fn.parameters = cleanedSchema;
} else {
fn.parametersJsonSchema = cleanedSchema;
delete fn.parameters;
}
fn.parameters = cleanedSchema;
// if (isClaude) {
// fn.parameters = cleanedSchema;
// } else {
// fn.parametersJsonSchema = cleanedSchema;
// delete fn.parameters;
// }
}
}
}
@@ -223,7 +240,7 @@ function openaiToGeminiCLIRequest(model, body, stream) {
// Wrap Gemini CLI format in Cloud Code wrapper
function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigravity = false) {
const projectId = credentials?.projectId || generateProjectId();
const envelope = {
project: projectId,
model: model,
@@ -241,7 +258,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
// Antigravity specific fields
if (isAntigravity) {
envelope.requestType = "agent";
// Inject required default system prompt for Antigravity
const defaultPart = { text: ANTIGRAVITY_DEFAULT_SYSTEM };
if (envelope.request.systemInstruction?.parts) {
@@ -249,7 +266,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
} else {
envelope.request.systemInstruction = { role: "user", parts: [defaultPart] };
}
// Add toolConfig for Antigravity
if (geminiCLI.tools?.length > 0) {
envelope.request.toolConfig = {
@@ -264,8 +281,119 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
return envelope;
}
// Wrap Claude format in Cloud Code envelope for Antigravity
function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = null) {
const projectId = credentials?.projectId || generateProjectId();
const envelope = {
project: projectId,
model: model,
userAgent: "antigravity",
requestId: `agent-${generateUUID()}`,
requestType: "agent",
request: {
sessionId: generateSessionId(),
contents: [],
generationConfig: {
temperature: claudeRequest.temperature || 1,
maxOutputTokens: claudeRequest.max_tokens || 4096
}
}
};
// Convert Claude messages to Gemini contents
if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
for (const msg of claudeRequest.messages) {
const parts = [];
if (Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "text") {
parts.push({ text: block.text });
} else if (block.type === "tool_use") {
parts.push({
functionCall: {
id: block.id,
name: block.name,
args: block.input || {}
}
});
} else if (block.type === "tool_result") {
let content = block.content;
if (Array.isArray(content)) {
content = content.map(c => c.type === "text" ? c.text : JSON.stringify(c)).join("\n");
}
parts.push({
functionResponse: {
id: block.tool_use_id,
name: "unknown",
response: { result: tryParseJSON(content) || content }
}
});
}
}
} else if (typeof msg.content === "string") {
parts.push({ text: msg.content });
}
if (parts.length > 0) {
envelope.request.contents.push({
role: msg.role === "assistant" ? "model" : "user",
parts
});
}
}
}
// Convert Claude tools to Gemini functionDeclarations
if (claudeRequest.tools && Array.isArray(claudeRequest.tools)) {
const functionDeclarations = [];
for (const tool of claudeRequest.tools) {
if (tool.name && tool.input_schema) {
const cleanedSchema = cleanJSONSchemaForAntigravity(tool.input_schema);
functionDeclarations.push({
name: tool.name,
description: tool.description || "",
parameters: cleanedSchema
});
}
}
if (functionDeclarations.length > 0) {
envelope.request.tools = [{ functionDeclarations }];
envelope.request.toolConfig = {
functionCallingConfig: { mode: "VALIDATED" }
};
}
}
// Add system instruction (Antigravity default)
const defaultPart = { text: ANTIGRAVITY_DEFAULT_SYSTEM };
const systemParts = [defaultPart];
if (claudeRequest.system) {
if (Array.isArray(claudeRequest.system)) {
for (const block of claudeRequest.system) {
if (block.text) systemParts.push({ text: block.text });
}
} else if (typeof claudeRequest.system === "string") {
systemParts.push({ text: claudeRequest.system });
}
}
envelope.request.systemInstruction = { role: "user", parts: systemParts };
return envelope;
}
// OpenAI -> Antigravity (Sandbox Cloud Code with wrapper)
function openaiToAntigravityRequest(model, body, stream, credentials = null) {
const isClaude = model.toLowerCase().includes("claude");
if (isClaude) {
const claudeRequest = openaiToClaudeRequestForAntigravity(model, body, stream);
return wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials);
}
const geminiCLI = openaiToGeminiCLIRequest(model, body, stream);
return wrapInCloudCodeEnvelope(model, geminiCLI, credentials, true);
}