mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
Fix Antigravity
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -56,6 +56,6 @@ cursor/*
|
||||
PUBLIC.md
|
||||
scripts/*
|
||||
Thanks.md
|
||||
package.json
|
||||
PUBLIC.en.md
|
||||
PR/*
|
||||
package-lock.json
|
||||
|
||||
6
CHANGELOG.md
Normal file
6
CHANGELOG.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# v0.2.21 (2026-01-12)
|
||||
|
||||
## Changes
|
||||
- Update ReadMe
|
||||
- Fix bug **antigravity**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user