From f9ef718fc692739fef22bfcfbbb611ccd5e677f9 Mon Sep 17 00:00:00 2001 From: decolua Date: Tue, 13 Jan 2026 17:19:41 +0700 Subject: [PATCH] Fix Antigravity --- open-sse/executors/antigravity.js | 17 ++++-- open-sse/handlers/chatCore.js | 10 +++- open-sse/translator/helpers/geminiHelper.js | 16 ++++- open-sse/utils/error.js | 66 +++++++++++++++++++-- 4 files changed, 94 insertions(+), 15 deletions(-) diff --git a/open-sse/executors/antigravity.js b/open-sse/executors/antigravity.js index 01e80f4..90c85c1 100644 --- a/open-sse/executors/antigravity.js +++ b/open-sse/executors/antigravity.js @@ -126,12 +126,17 @@ export class AntigravityExecutor extends BaseExecutor { let lastError = null; let lastStatus = 0; const MAX_AUTO_RETRIES = 2; + const retryAttemptsByUrl = {}; // Track retry attempts per URL 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; + + // Initialize retry counter for this URL + if (!retryAttemptsByUrl[urlIndex]) { + retryAttemptsByUrl[urlIndex] = 0; + } try { const response = await fetch(url, { @@ -152,10 +157,12 @@ export class AntigravityExecutor extends BaseExecutor { } // 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)); + if (response.status === 429 && (!retryMs || retryMs === 0) && retryAttemptsByUrl[urlIndex] < MAX_AUTO_RETRIES) { + retryAttemptsByUrl[urlIndex]++; + // Exponential backoff: 2s, 4s, 8s... + const backoffMs = Math.min(1000 * Math.pow(2, retryAttemptsByUrl[urlIndex]), MAX_RETRY_AFTER_MS); + log?.debug?.("RETRY", `429 auto retry ${retryAttemptsByUrl[urlIndex]}/${MAX_AUTO_RETRIES} after ${backoffMs/1000}s`); + await new Promise(resolve => setTimeout(resolve, backoffMs)); urlIndex--; continue; } diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js index d929bae..6231bb9 100644 --- a/open-sse/handlers/chatCore.js +++ b/open-sse/handlers/chatCore.js @@ -229,15 +229,21 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred // Check provider response - return error info for fallback handling if (!providerResponse.ok) { trackPendingRequest(model, provider, connectionId, false); - const { statusCode, message } = await parseUpstreamError(providerResponse); + const { statusCode, message, retryAfterMs } = await parseUpstreamError(providerResponse, provider); appendRequestLog({ model, provider, connectionId, status: `FAILED ${statusCode}` }).catch(() => {}); const errMsg = formatProviderError(new Error(message), provider, model, statusCode); console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`); + // Log Antigravity retry time if available + if (retryAfterMs && provider === "antigravity") { + const retrySeconds = Math.ceil(retryAfterMs / 1000); + log?.debug?.("RETRY", `Antigravity quota reset in ${retrySeconds}s (${retryAfterMs}ms)`); + } + // Log error with full request body for debugging reqLogger.logError(new Error(message), finalBody || translatedBody); - return createErrorResult(statusCode, errMsg); + return createErrorResult(statusCode, errMsg, retryAfterMs); } // Non-streaming response diff --git a/open-sse/translator/helpers/geminiHelper.js b/open-sse/translator/helpers/geminiHelper.js index 5f130cf..52e1790 100644 --- a/open-sse/translator/helpers/geminiHelper.js +++ b/open-sse/translator/helpers/geminiHelper.js @@ -1,11 +1,23 @@ // Gemini helper functions for translator // Unsupported JSON Schema constraints that should be removed for Antigravity +// Reference: CLIProxyAPI/internal/util/gemini_schema.go (removeUnsupportedKeywords) export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [ + // Basic constraints (not supported by Gemini API) "minLength", "maxLength", "exclusiveMinimum", "exclusiveMaximum", "pattern", "minItems", "maxItems", "format", - "default", "examples", "$schema", "const", "title", - "anyOf", "oneOf", "allOf", "not" + // Claude rejects these in VALIDATED mode + "default", "examples", + // JSON Schema meta keywords + "$schema", "$defs", "definitions", "const", "$ref", + // Object validation keywords (not supported) + "additionalProperties", "propertyNames", "patternProperties", + // Complex schema keywords (handled by flattenAnyOfOneOf/mergeAllOf) + "anyOf", "oneOf", "allOf", "not", + // Dependency keywords (not supported) + "dependencies", "dependentSchemas", "dependentRequired", + // Other unsupported keywords + "title", "if", "then", "else", "contentMediaType", "contentEncoding" ]; // Default safety settings diff --git a/open-sse/utils/error.js b/open-sse/utils/error.js index 75b7f85..262fc75 100644 --- a/open-sse/utils/error.js +++ b/open-sse/utils/error.js @@ -78,13 +78,51 @@ export async function writeStreamError(writer, statusCode, message) { await writer.write(encoder.encode(`data: ${JSON.stringify(errorBody)}\n\n`)); } +/** + * Parse Antigravity error message to extract retry time + * Example: "You have exhausted your capacity on this model. Your quota will reset after 2h7m23s." + * @param {string} message - Error message + * @returns {number|null} Retry time in milliseconds, or null if not found + */ +export function parseAntigravityRetryTime(message) { + if (typeof message !== "string") return null; + + // Match patterns like: 2h7m23s, 5m30s, 45s, 1h20m, etc. + const match = message.match(/reset after (\d+h)?(\d+m)?(\d+s)?/i); + if (!match) return null; + + let totalMs = 0; + + // Extract hours + if (match[1]) { + const hours = parseInt(match[1]); + totalMs += hours * 60 * 60 * 1000; + } + + // Extract minutes + if (match[2]) { + const minutes = parseInt(match[2]); + totalMs += minutes * 60 * 1000; + } + + // Extract seconds + if (match[3]) { + const seconds = parseInt(match[3]); + totalMs += seconds * 1000; + } + + return totalMs > 0 ? totalMs : null; +} + /** * Parse upstream provider error response * @param {Response} response - Fetch response from provider - * @returns {Promise<{statusCode: number, message: string}>} + * @param {string} provider - Provider name (for Antigravity-specific parsing) + * @returns {Promise<{statusCode: number, message: string, retryAfterMs: number|null}>} */ -export async function parseUpstreamError(response) { +export async function parseUpstreamError(response, provider = null) { let message = ""; + let retryAfterMs = null; try { const text = await response.text(); @@ -100,9 +138,17 @@ export async function parseUpstreamError(response) { message = `Upstream error: ${response.status}`; } + const messageStr = typeof message === "string" ? message : JSON.stringify(message); + + // Parse Antigravity-specific retry time from error message + if (provider === "antigravity" && response.status === 429) { + retryAfterMs = parseAntigravityRetryTime(messageStr); + } + return { statusCode: response.status, - message: typeof message === "string" ? message : JSON.stringify(message) + message: messageStr, + retryAfterMs }; } @@ -110,15 +156,23 @@ export async function parseUpstreamError(response) { * Create error result for chatCore handler * @param {number} statusCode - HTTP status code * @param {string} message - Error message - * @returns {{ success: false, status: number, error: string, response: Response }} + * @param {number|null} retryAfterMs - Optional retry-after time in milliseconds + * @returns {{ success: false, status: number, error: string, response: Response, retryAfterMs?: number }} */ -export function createErrorResult(statusCode, message) { - return { +export function createErrorResult(statusCode, message, retryAfterMs = null) { + const result = { success: false, status: statusCode, error: message, response: errorResponse(statusCode, message) }; + + // Add retryAfterMs if available (for Antigravity quota errors) + if (retryAfterMs) { + result.retryAfterMs = retryAfterMs; + } + + return result; } /**