fix(antigravity): retry transient upstream failures

Retry short-lived 5xx/capacity errors (500/502/503/504 + message
patterns) with bounded backoff capped at 15s; honor Retry-After/reset
hints and skip when wait is too long. Keep 400 non-retryable. Enable the
retry hook for 500 alongside existing 429/503.

Deduplicate sanitized Antigravity tool names before emitting the single
functionDeclarations group to avoid upstream "Tool names must be unique"
rejections.

Add Headroom size diagnostics and phantom-savings warning when reported
token delta does not shrink the outbound payload.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Sutarto Jordan Chrisfivo
2026-06-26 17:16:44 +07:00
committed by decolua
parent 2deacf69b1
commit 639f1204d0
4 changed files with 116 additions and 22 deletions

View File

@@ -16,8 +16,26 @@ function sanitizeFunctionName(name) {
}
const MAX_RETRY_AFTER_MS = 10000;
const ANTIGRAVITY_TRANSIENT_RETRY_MAX_MS = 15000;
const MAX_ANTIGRAVITY_OUTPUT_TOKENS = 16384;
const ANTIGRAVITY_TRANSIENT_ERROR_PATTERNS = [
/high\s+traffic/i,
/agent\s+(execution\s+)?terminated\s+due\s+to\s+error/i,
/capacity/i,
/temporarily\s+unavailable/i,
/timeout/i,
/stream\s+(ended|closed|terminated|interrupted)/i,
/empty\s+response/i,
];
const ANTIGRAVITY_TRANSIENT_STATUSES = new Set([
HTTP_STATUS.SERVER_ERROR,
HTTP_STATUS.BAD_GATEWAY,
HTTP_STATUS.SERVICE_UNAVAILABLE,
HTTP_STATUS.GATEWAY_TIMEOUT,
]);
// Fields Google generateContent rejects (Claude/OpenAI/Qwen thinking fields set at body root by thinkingUnified.js)
const ANTIGRAVITY_REQUEST_BLACKLIST = [
"output_config",
@@ -170,15 +188,22 @@ export class AntigravityExecutor extends BaseExecutor {
if (tools && tools.length > 0) {
// Merge all groups into a single functionDeclarations group (Gemini expects 1 group)
const allDeclarations = tools.flatMap(group =>
(group.functionDeclarations || []).map(fn => ({
...fn,
name: sanitizeFunctionName(fn.name),
parameters: fn.parameters
? cleanJSONSchemaForAntigravity(structuredClone(fn.parameters))
: { type: "object", properties: { reason: { type: "string", description: "Brief explanation" } }, required: ["reason"] }
}))
);
const seenToolNames = new Set();
const allDeclarations = [];
for (const group of tools) {
for (const fn of group.functionDeclarations || []) {
const name = sanitizeFunctionName(fn.name);
if (seenToolNames.has(name)) continue;
seenToolNames.add(name);
allDeclarations.push({
...fn,
name,
parameters: fn.parameters
? cleanJSONSchemaForAntigravity(structuredClone(fn.parameters))
: { type: "object", properties: { reason: { type: "string", description: "Brief explanation" } }, required: ["reason"] }
});
}
}
tools = allDeclarations.length > 0 ? [{ functionDeclarations: allDeclarations }] : [];
}
@@ -305,23 +330,49 @@ export class AntigravityExecutor extends BaseExecutor {
return totalMs > 0 ? totalMs : null;
}
extractErrorMessage(errorJson, bodyText = "") {
return [
errorJson?.error?.message,
errorJson?.message,
errorJson?.error,
bodyText,
].filter(Boolean).map(v => typeof v === "string" ? v : JSON.stringify(v)).join("\n");
}
isTransientAntigravityError(status, message) {
if (status === HTTP_STATUS.RATE_LIMITED) return true;
if (ANTIGRAVITY_TRANSIENT_STATUSES.has(status)) return true;
return ANTIGRAVITY_TRANSIENT_ERROR_PATTERNS.some(pattern => pattern.test(message || ""));
}
// Hook called by BaseExecutor.tryRetry: derive delay from Retry-After (header → body),
// cap at MAX_RETRY_AFTER_MS, else exponential backoff for 429. Return false to veto (fallback URL).
// cap at MAX_RETRY_AFTER_MS, else retry transient Antigravity failures with backoff.
// Return false to veto (fallback URL / final error).
async computeRetryDelay(response, attempt) {
let bodyText = "";
let errorJson = null;
let retryMs = this.parseRetryHeaders(response.headers);
try {
bodyText = await response.clone().text();
errorJson = bodyText ? JSON.parse(bodyText) : null;
} catch {
// ignore parse errors → fall through to status/message based retry
}
const errorMessage = this.extractErrorMessage(errorJson, bodyText);
if (!retryMs) {
try {
const errorJson = JSON.parse(await response.clone().text());
retryMs = this.parseRetryFromErrorMessage(errorJson?.error?.message || errorJson?.message || "");
} catch {
// ignore parse errors → fall through to backoff
}
retryMs = this.parseRetryFromErrorMessage(errorMessage);
}
if (retryMs) return retryMs <= MAX_RETRY_AFTER_MS ? retryMs : false;
if (response.status === HTTP_STATUS.RATE_LIMITED) {
return Math.min(1000 * (2 ** attempt), MAX_RETRY_AFTER_MS); // exponential backoff
}
return false;
if (!this.isTransientAntigravityError(response.status, errorMessage)) return false;
const cap = response.status === HTTP_STATUS.RATE_LIMITED
? MAX_RETRY_AFTER_MS
: ANTIGRAVITY_TRANSIENT_RETRY_MAX_MS;
return Math.min(1000 * (2 ** attempt), cap); // exponential backoff
}
/**

View File

@@ -32,6 +32,9 @@ export default {
"429": {
attempts: 3,
},
"500": {
attempts: 3,
},
"503": {
attempts: 3,
},