| 72 | * @throws The last error encountered if all attempts fail. |
| 73 | */ |
| 74 | export async function retryWithBackoff<T>( |
| 75 | fn: () => Promise<T>, |
| 76 | options?: Partial<RetryOptions>, |
| 77 | ): Promise<T> { |
| 78 | const { |
| 79 | maxAttempts, |
| 80 | initialDelayMs, |
| 81 | maxDelayMs, |
| 82 | onPersistent429, |
| 83 | authType, |
| 84 | shouldRetry, |
| 85 | } = { |
| 86 | ...DEFAULT_RETRY_OPTIONS, |
| 87 | ...options, |
| 88 | }; |
| 89 | |
| 90 | let attempt = 0; |
| 91 | let currentDelay = initialDelayMs; |
| 92 | let consecutive429Count = 0; |
| 93 | |
| 94 | while (attempt < maxAttempts) { |
| 95 | attempt++; |
| 96 | try { |
| 97 | return await fn(); |
| 98 | } catch (error) { |
| 99 | const errorStatus = getErrorStatus(error); |
| 100 | |
| 101 | // Check for Pro quota exceeded error first - immediate fallback for OAuth users |
| 102 | if ( |
| 103 | errorStatus === 429 && |
| 104 | authType === AuthType.LOGIN_WITH_GOOGLE && |
| 105 | isProQuotaExceededError(error) && |
| 106 | onPersistent429 |
| 107 | ) { |
| 108 | try { |
| 109 | const fallbackModel = await onPersistent429(authType, error); |
| 110 | if (fallbackModel !== false && fallbackModel !== null) { |
| 111 | // Reset attempt counter and try with new model |
| 112 | attempt = 0; |
| 113 | consecutive429Count = 0; |
| 114 | currentDelay = initialDelayMs; |
| 115 | // With the model updated, we continue to the next attempt |
| 116 | continue; |
| 117 | } else { |
| 118 | // Fallback handler returned null/false, meaning don't continue - stop retry process |
| 119 | throw error; |
| 120 | } |
| 121 | } catch (fallbackError) { |
| 122 | // If fallback fails, continue with original error |
| 123 | console.warn('Fallback to Flash model failed:', fallbackError); |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | // Check for generic quota exceeded error (but not Pro, which was handled above) - immediate fallback for OAuth users |
| 128 | if ( |
| 129 | errorStatus === 429 && |
| 130 | authType === AuthType.LOGIN_WITH_GOOGLE && |
| 131 | !isProQuotaExceededError(error) && |