feat(antigravity): add GCP permission error retry with exponential backoff
- Add retry logic for 403 GCP permission errors (max 10 retries) - Implement exponential backoff with 2s cap (200ms → 400ms → 800ms → 2000ms) - Detect patterns: PERMISSION_DENIED, Cloud AI Companion API not enabled, etc. 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -70,6 +70,21 @@ function isRetryableError(status: number): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GCP_PERMISSION_ERROR_PATTERNS = [
|
||||||
|
"PERMISSION_DENIED",
|
||||||
|
"does not have permission",
|
||||||
|
"Cloud AI Companion API has not been used",
|
||||||
|
"has not been enabled",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function isGcpPermissionError(text: string): boolean {
|
||||||
|
return GCP_PERMISSION_ERROR_PATTERNS.some((pattern) => text.includes(pattern))
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRetryDelay(attempt: number): number {
|
||||||
|
return Math.min(200 * Math.pow(2, attempt), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
async function isRetryableResponse(response: Response): Promise<boolean> {
|
async function isRetryableResponse(response: Response): Promise<boolean> {
|
||||||
if (isRetryableError(response.status)) return true
|
if (isRetryableError(response.status)) return true
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
@@ -155,23 +170,43 @@ async function attemptFetch(
|
|||||||
|
|
||||||
debugLog(`[REQ] streaming=${transformed.streaming}, url=${transformed.url}`)
|
debugLog(`[REQ] streaming=${transformed.streaming}, url=${transformed.url}`)
|
||||||
|
|
||||||
const response = await fetch(transformed.url, {
|
const maxPermissionRetries = 10
|
||||||
method: init.method || "POST",
|
for (let attempt = 0; attempt <= maxPermissionRetries; attempt++) {
|
||||||
headers: transformed.headers,
|
const response = await fetch(transformed.url, {
|
||||||
body: JSON.stringify(transformed.body),
|
method: init.method || "POST",
|
||||||
signal: init.signal,
|
headers: transformed.headers,
|
||||||
})
|
body: JSON.stringify(transformed.body),
|
||||||
|
signal: init.signal,
|
||||||
|
})
|
||||||
|
|
||||||
debugLog(
|
debugLog(
|
||||||
`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`
|
`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!response.ok && (await isRetryableResponse(response))) {
|
if (response.status === 403) {
|
||||||
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
|
try {
|
||||||
return null
|
const text = await response.clone().text()
|
||||||
|
if (isGcpPermissionError(text)) {
|
||||||
|
if (attempt < maxPermissionRetries) {
|
||||||
|
const delay = calculateRetryDelay(attempt)
|
||||||
|
debugLog(`[RETRY] GCP permission error, retry ${attempt + 1}/${maxPermissionRetries} after ${delay}ms`)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
debugLog(`[RETRY] GCP permission error, max retries exceeded`)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok && (await isRetryableResponse(response))) {
|
||||||
|
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLog(
|
debugLog(
|
||||||
`Endpoint failed: ${endpoint} (${error instanceof Error ? error.message : "Unknown error"}), trying next`
|
`Endpoint failed: ${endpoint} (${error instanceof Error ? error.message : "Unknown error"}), trying next`
|
||||||
|
|||||||
Reference in New Issue
Block a user