Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/core/src/core/contentGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 10 additions & 7 deletions packages/core/src/core/geminiChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -316,26 +322,23 @@ 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 {
type: StreamEventType.RETRY,
retryInfo: {
message,
attempt: rateLimitRetryCount,
maxRetries: RATE_LIMIT_RETRY_OPTIONS.maxRetries,
maxRetries: maxRateLimitRetries,
delayMs,
},
};
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/models/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const MODEL_GENERATION_CONFIG_FIELDS = [
'samplingParams',
'timeout',
'maxRetries',
'retryErrorCodes',
'enableCacheControl',
'schemaCompliance',
'reasoning',
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type ModelGenerationConfig = Pick<
| 'samplingParams'
| 'timeout'
| 'maxRetries'
| 'retryErrorCodes'
| 'enableCacheControl'
| 'schemaCompliance'
| 'reasoning'
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/utils/rateLimit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
17 changes: 14 additions & 3 deletions packages/core/src/utils/rateLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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;
}

/**
Expand Down