Skip to content

Commit e533a35

Browse files
committed
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)
1 parent 934d4bc commit e533a35

File tree

1 file changed

+48
-13
lines changed

1 file changed

+48
-13
lines changed

src/auth/antigravity/fetch.ts

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ function isRetryableError(status: number): boolean {
7070
return false
7171
}
7272

73+
const GCP_PERMISSION_ERROR_PATTERNS = [
74+
"PERMISSION_DENIED",
75+
"does not have permission",
76+
"Cloud AI Companion API has not been used",
77+
"has not been enabled",
78+
] as const
79+
80+
function isGcpPermissionError(text: string): boolean {
81+
return GCP_PERMISSION_ERROR_PATTERNS.some((pattern) => text.includes(pattern))
82+
}
83+
84+
function calculateRetryDelay(attempt: number): number {
85+
return Math.min(200 * Math.pow(2, attempt), 2000)
86+
}
87+
7388
async function isRetryableResponse(response: Response): Promise<boolean> {
7489
if (isRetryableError(response.status)) return true
7590
if (response.status === 403) {
@@ -155,23 +170,43 @@ async function attemptFetch(
155170

156171
debugLog(`[REQ] streaming=${transformed.streaming}, url=${transformed.url}`)
157172

158-
const response = await fetch(transformed.url, {
159-
method: init.method || "POST",
160-
headers: transformed.headers,
161-
body: JSON.stringify(transformed.body),
162-
signal: init.signal,
163-
})
173+
const maxPermissionRetries = 10
174+
for (let attempt = 0; attempt <= maxPermissionRetries; attempt++) {
175+
const response = await fetch(transformed.url, {
176+
method: init.method || "POST",
177+
headers: transformed.headers,
178+
body: JSON.stringify(transformed.body),
179+
signal: init.signal,
180+
})
164181

165-
debugLog(
166-
`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`
167-
)
182+
debugLog(
183+
`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`
184+
)
185+
186+
if (response.status === 403) {
187+
try {
188+
const text = await response.clone().text()
189+
if (isGcpPermissionError(text)) {
190+
if (attempt < maxPermissionRetries) {
191+
const delay = calculateRetryDelay(attempt)
192+
debugLog(`[RETRY] GCP permission error, retry ${attempt + 1}/${maxPermissionRetries} after ${delay}ms`)
193+
await new Promise((resolve) => setTimeout(resolve, delay))
194+
continue
195+
}
196+
debugLog(`[RETRY] GCP permission error, max retries exceeded`)
197+
}
198+
} catch {}
199+
}
168200

169-
if (!response.ok && (await isRetryableResponse(response))) {
170-
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
171-
return null
201+
if (!response.ok && (await isRetryableResponse(response))) {
202+
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
203+
return null
204+
}
205+
206+
return response
172207
}
173208

174-
return response
209+
return null
175210
} catch (error) {
176211
debugLog(
177212
`Endpoint failed: ${endpoint} (${error instanceof Error ? error.message : "Unknown error"}), trying next`

0 commit comments

Comments
 (0)