diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index f3af06bda2b..ad2326de3f1 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -70,7 +70,8 @@ export type ContentGeneratorConfig = { enableOpenAILogging?: boolean; openAILoggingDir?: string; timeout?: number; // Timeout configuration in milliseconds - maxRetries?: number; // Maximum retries for failed requests + maxRetries?: number; // Maximum retries for rate-limit errors + retryErrorCodes?: number[]; // Additional error codes that trigger rate-limit retry enableCacheControl?: boolean; // Enable cache control for DashScope providers samplingParams?: { top_p?: number; diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index 0bac7066fea..2e192335515 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -286,6 +286,12 @@ export class GeminiChat { let lastError: unknown = new Error('Request failed after all retries.'); let rateLimitRetryCount = 0; + // Read per-config overrides; fall back to built-in defaults. + const cgConfig = self.config.getContentGeneratorConfig(); + const maxRateLimitRetries = + cgConfig?.maxRetries ?? RATE_LIMIT_RETRY_OPTIONS.maxRetries; + const extraRetryErrorCodes = cgConfig?.retryErrorCodes; + for ( let attempt = 0; attempt < INVALID_CONTENT_RETRY_OPTIONS.maxAttempts; @@ -316,18 +322,15 @@ export class GeminiChat { // These arrive as StreamContentError with finish_reason="error_finish" // from the pipeline, containing the throttling message in the content. // Covers TPM throttling, GLM rate limits, and other provider throttling. - const isRateLimit = isRateLimitError(error); - if ( - isRateLimit && - rateLimitRetryCount < RATE_LIMIT_RETRY_OPTIONS.maxRetries - ) { + const isRateLimit = isRateLimitError(error, extraRetryErrorCodes); + if (isRateLimit && rateLimitRetryCount < maxRateLimitRetries) { rateLimitRetryCount++; const delayMs = RATE_LIMIT_RETRY_OPTIONS.delayMs; const message = parseAndFormatApiError( error instanceof Error ? error.message : String(error), ); debugLogger.warn( - `Rate limit throttling detected (retry ${rateLimitRetryCount}/${RATE_LIMIT_RETRY_OPTIONS.maxRetries}). ` + + `Rate limit throttling detected (retry ${rateLimitRetryCount}/${maxRateLimitRetries}). ` + `Waiting ${delayMs / 1000}s before retrying...`, ); yield { @@ -335,7 +338,7 @@ export class GeminiChat { retryInfo: { message, attempt: rateLimitRetryCount, - maxRetries: RATE_LIMIT_RETRY_OPTIONS.maxRetries, + maxRetries: maxRateLimitRetries, delayMs, }, }; diff --git a/packages/core/src/models/constants.ts b/packages/core/src/models/constants.ts index 025e3b9cfd6..00a00ed9485 100644 --- a/packages/core/src/models/constants.ts +++ b/packages/core/src/models/constants.ts @@ -22,6 +22,7 @@ export const MODEL_GENERATION_CONFIG_FIELDS = [ 'samplingParams', 'timeout', 'maxRetries', + 'retryErrorCodes', 'enableCacheControl', 'schemaCompliance', 'reasoning', diff --git a/packages/core/src/models/types.ts b/packages/core/src/models/types.ts index 69c286729f9..61509c44bd7 100644 --- a/packages/core/src/models/types.ts +++ b/packages/core/src/models/types.ts @@ -29,6 +29,7 @@ export type ModelGenerationConfig = Pick< | 'samplingParams' | 'timeout' | 'maxRetries' + | 'retryErrorCodes' | 'enableCacheControl' | 'schemaCompliance' | 'reasoning' diff --git a/packages/core/src/utils/rateLimit.test.ts b/packages/core/src/utils/rateLimit.test.ts index 48605db20e1..a342a4a0b00 100644 --- a/packages/core/src/utils/rateLimit.test.ts +++ b/packages/core/src/utils/rateLimit.test.ts @@ -33,6 +33,13 @@ describe('isRateLimitError — detection paths', () => { expect(info).toBe(true); }); + it('should detect 1305 code from ApiError (issue #1918)', () => { + const info = isRateLimitError({ + error: { code: 1305, message: 'IdealTalk rate limit' }, + }); + expect(info).toBe(true); + }); + it('should detect rate-limit from StructuredError.status', () => { const error: StructuredError = { message: 'Rate limited', status: 429 }; const info = isRateLimitError(error); @@ -52,6 +59,21 @@ describe('isRateLimitError — detection paths', () => { ).toBe(false); }); + it('should detect custom error code passed via extraCodes', () => { + expect( + isRateLimitError( + { error: { code: 9999, message: 'Custom rate limit' } }, + [9999], + ), + ).toBe(true); + }); + + it('should not detect custom code when extraCodes is not provided', () => { + expect( + isRateLimitError({ error: { code: 9999, message: 'Custom rate limit' } }), + ).toBe(false); + }); + it('should return null for invalid inputs', () => { expect(isRateLimitError(null)).toBe(false); expect(isRateLimitError(undefined)).toBe(false); diff --git a/packages/core/src/utils/rateLimit.ts b/packages/core/src/utils/rateLimit.ts index 559cb26fb84..19466e90f6c 100644 --- a/packages/core/src/utils/rateLimit.ts +++ b/packages/core/src/utils/rateLimit.ts @@ -10,7 +10,8 @@ import { isApiError, isStructuredError } from './quotaErrorDetection.js'; // 429 - Standard HTTP "Too Many Requests" (DashScope TPM, OpenAI, etc.) // 503 - Provider throttling/overload (treated as rate-limit for retry UI) // 1302 - Z.AI GLM rate limit (https://docs.z.ai/api-reference/api-code) -const RATE_LIMIT_ERROR_CODES = new Set([429, 503, 1302]); +// 1305 - DashScope/IdealTalk internal rate limit (issue #1918) +const RATE_LIMIT_ERROR_CODES = new Set([429, 503, 1302, 1305]); export interface RetryInfo { /** Formatted error message for display, produced by parseAndFormatApiError. */ @@ -25,10 +26,20 @@ export interface RetryInfo { /** * Detects rate-limit / throttling errors and returns retry info. + * + * @param error - The error to check. + * @param extraCodes - Additional error codes to treat as rate-limit errors, + * merged with the built-in set at call time (not mutating the default set). */ -export function isRateLimitError(error: unknown): boolean { +export function isRateLimitError( + error: unknown, + extraCodes?: readonly number[], +): boolean { const code = getErrorCode(error); - return code !== null && RATE_LIMIT_ERROR_CODES.has(code); + if (code === null) return false; + if (RATE_LIMIT_ERROR_CODES.has(code)) return true; + if (extraCodes && extraCodes.includes(code)) return true; + return false; } /**