diff --git a/.gitignore b/.gitignore index 28c8ebc..4751dac 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,6 @@ cursor/* PUBLIC.md scripts/* Thanks.md -package.json PUBLIC.en.md PR/* +package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a90a7eb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# v0.2.21 (2026-01-12) + +## Changes +- Update ReadMe +- Fix bug **antigravity** + diff --git a/open-sse/executors/antigravity.js b/open-sse/executors/antigravity.js index a48d7a2..7688c47 100644 --- a/open-sse/executors/antigravity.js +++ b/open-sse/executors/antigravity.js @@ -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; diff --git a/open-sse/translator/request/openai-to-claude.js b/open-sse/translator/request/openai-to-claude.js index 25a87a8..91c9b88 100644 --- a/open-sse/translator/request/openai-to-claude.js +++ b/open-sse/translator/request/openai-to-claude.js @@ -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); diff --git a/open-sse/translator/request/openai-to-gemini.js b/open-sse/translator/request/openai-to-gemini.js index d267df2..fa6bde4 100644 --- a/open-sse/translator/request/openai-to-gemini.js +++ b/open-sse/translator/request/openai-to-gemini.js @@ -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); }