feat(providers): implemented time-budget retry for Gemini 429 errors
- Added time-budget-based retry strategy for Google Gemini 429 rate limit errors with 5-minute window. - Set default maxRetries to 5 for Anthropic, Azure OpenAI, and OpenAI clients. - Implemented separate 429 rate limit handling with cumulative time budget tracking instead of bounded retry attempts.
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- Implemented time-budget-based retry strategy for Google Gemini rate limits (429 errors), allowing retries within a 5-minute window instead of a fixed attempt cap
|
||||
- Set default `maxRetries` to 5 for Anthropic, Azure OpenAI, and OpenAI client configurations to improve resilience against transient failures
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved error messages for OAuth token refresh failures by including detailed error information from the provider
|
||||
|
||||
@@ -629,6 +629,7 @@ function createClient(
|
||||
baseURL: config.baseURL,
|
||||
defaultHeaders: config.defaultHeaders,
|
||||
dangerouslyAllowBrowser: config.dangerouslyAllowBrowser,
|
||||
maxRetries: 5,
|
||||
});
|
||||
|
||||
return { client, isOAuthToken: config.isOAuthToken };
|
||||
|
||||
@@ -424,6 +424,7 @@ function createClient(model: Model<"azure-openai-responses">, apiKey: string, op
|
||||
apiKey,
|
||||
apiVersion,
|
||||
dangerouslyAllowBrowser: true,
|
||||
maxRetries: 5,
|
||||
defaultHeaders: headers,
|
||||
baseURL: baseUrl,
|
||||
});
|
||||
|
||||
@@ -99,6 +99,7 @@ const MAX_RETRIES = 3;
|
||||
const BASE_DELAY_MS = 1000;
|
||||
const MAX_EMPTY_STREAM_RETRIES = 2;
|
||||
const EMPTY_STREAM_BASE_DELAY_MS = 500;
|
||||
const RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000;
|
||||
const CLAUDE_THINKING_BETA_HEADER = "interleaved-thinking-2025-05-14";
|
||||
|
||||
/**
|
||||
@@ -360,8 +361,9 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
||||
let response: Response | undefined;
|
||||
let lastError: Error | undefined;
|
||||
let requestUrl: string | undefined;
|
||||
let rateLimitTimeSpent = 0;
|
||||
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
if (options?.signal?.aborted) {
|
||||
throw new Error("Request was aborted");
|
||||
}
|
||||
@@ -382,13 +384,25 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
||||
|
||||
const errorText = await response.text();
|
||||
|
||||
// Check if retryable
|
||||
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
|
||||
// Use server-provided delay or exponential backoff
|
||||
// Handle 429 rate limits with time budget
|
||||
if (response.status === 429) {
|
||||
const serverDelay = extractRetryDelay(errorText, response);
|
||||
if (serverDelay && rateLimitTimeSpent + serverDelay <= RATE_LIMIT_BUDGET_MS) {
|
||||
rateLimitTimeSpent += serverDelay;
|
||||
await abortableSleep(serverDelay, options?.signal);
|
||||
continue;
|
||||
}
|
||||
// Fallback: use exponential backoff if no server delay, up to MAX_RETRIES
|
||||
if (!serverDelay && attempt < MAX_RETRIES) {
|
||||
await abortableSleep(BASE_DELAY_MS * 2 ** attempt, options?.signal);
|
||||
continue;
|
||||
}
|
||||
} else if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
|
||||
// Non-429 retryable errors use standard attempt cap
|
||||
const serverDelay = extractRetryDelay(errorText, response);
|
||||
const delayMs = serverDelay ?? BASE_DELAY_MS * 2 ** attempt;
|
||||
|
||||
// Check if server delay exceeds max allowed (default: 60s)
|
||||
// Check if server delay exceeds max allowed (default: 60s) for non-429 errors
|
||||
const maxDelayMs = options?.maxRetryDelayMs ?? 60000;
|
||||
if (maxDelayMs > 0 && serverDelay && serverDelay > maxDelayMs) {
|
||||
const delaySeconds = Math.ceil(serverDelay / 1000);
|
||||
@@ -401,7 +415,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not retryable or max retries exceeded
|
||||
// Not retryable or budget exceeded
|
||||
throw new Error(`Cloud Code Assist API error (${response.status}): ${extractErrorMessage(errorText)}`);
|
||||
} catch (error) {
|
||||
// Check for abort - fetch throws AbortError, our code throws "Request was aborted"
|
||||
|
||||
@@ -480,6 +480,7 @@ async function createClient(
|
||||
apiKey,
|
||||
baseURL: model.baseUrl,
|
||||
dangerouslyAllowBrowser: true,
|
||||
maxRetries: 5,
|
||||
defaultHeaders: headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -397,6 +397,7 @@ function createClient(
|
||||
apiKey,
|
||||
baseURL: model.baseUrl,
|
||||
dangerouslyAllowBrowser: true,
|
||||
maxRetries: 5,
|
||||
defaultHeaders: headers,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user