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); } buildUrl(model, stream, urlIndex = 0) { const baseUrls = this.getBaseUrls(); const baseUrl = baseUrls[urlIndex] || baseUrls[0]; const path = stream ? "/v1internal:streamGenerateContent?alt=sse" : "/v1internal:generateContent"; return `${baseUrl}${path}`; } buildHeaders(credentials, stream = true) { return { "Content-Type": "application/json", "Authorization": `Bearer ${credentials.accessToken}`, "User-Agent": this.config.headers?.["User-Agent"] || "antigravity/1.104.0 darwin/arm64", ...(stream && { "Accept": "text/event-stream" }) }; } 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, model: model, userAgent: "antigravity", requestType: "agent", requestId: `agent-${crypto.randomUUID()}`, request: transformedRequest }; } async refreshCredentials(credentials, log) { if (!credentials.refreshToken) return null; try { const response = await fetch(OAUTH_ENDPOINTS.google.token, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: credentials.refreshToken, client_id: this.config.clientId, client_secret: this.config.clientSecret }) }); if (!response.ok) return null; const tokens = await response.json(); log?.info?.("TOKEN", "Antigravity refreshed"); return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token || credentials.refreshToken, expiresIn: tokens.expires_in, projectId: credentials.projectId }; } catch (error) { log?.error?.("TOKEN", `Antigravity refresh error: ${error.message}`); return null; } } generateProjectId() { const adj = ["useful", "bright", "swift", "calm", "bold"][Math.floor(Math.random() * 5)]; const noun = ["fuze", "wave", "spark", "flow", "core"][Math.floor(Math.random() * 5)]; return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`; } 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;