Skip to content

Commit ed4b440

Browse files
fix(patch): cherry-pick 9e6914d to release/v0.22.0-preview.2-pr-15288 to patch version v0.22.0-preview.2 and create version 0.22.0-preview.3 (#15294)
Co-authored-by: Sehoon Shon <[email protected]>
1 parent a6841f4 commit ed4b440

File tree

2 files changed

+92
-2
lines changed

2 files changed

+92
-2
lines changed

packages/core/src/utils/googleQuotaErrors.test.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ describe('classifyGoogleError', () => {
325325
expect(result).toBeInstanceOf(TerminalQuotaError);
326326
});
327327

328-
it('should return original error for 429 without specific details', () => {
328+
it('should return RetryableQuotaError for any 429', () => {
329329
const apiError: GoogleApiError = {
330330
code: 429,
331331
message: 'Too many requests',
@@ -340,7 +340,10 @@ describe('classifyGoogleError', () => {
340340
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(apiError);
341341
const originalError = new Error();
342342
const result = classifyGoogleError(originalError);
343-
expect(result).toBe(originalError);
343+
expect(result).toBeInstanceOf(RetryableQuotaError);
344+
if (result instanceof RetryableQuotaError) {
345+
expect(result.retryDelayMs).toBe(5000);
346+
}
344347
});
345348

346349
it('should classify nested JSON string 404 error as ModelNotFoundError', () => {
@@ -389,4 +392,61 @@ describe('classifyGoogleError', () => {
389392
});
390393
}
391394
});
395+
396+
it('should return RetryableQuotaError with 5s fallback for generic 429 without specific message', () => {
397+
const generic429 = {
398+
status: 429,
399+
message: 'Resource exhausted. No specific retry info.',
400+
};
401+
402+
const result = classifyGoogleError(generic429);
403+
404+
expect(result).toBeInstanceOf(RetryableQuotaError);
405+
if (result instanceof RetryableQuotaError) {
406+
expect(result.retryDelayMs).toBe(5000);
407+
}
408+
});
409+
410+
it('should return RetryableQuotaError with 5s fallback for 429 with empty details and no regex match', () => {
411+
const errorWithEmptyDetails = {
412+
error: {
413+
code: 429,
414+
message: 'A generic 429 error with no retry message.',
415+
details: [],
416+
},
417+
};
418+
419+
const result = classifyGoogleError(errorWithEmptyDetails);
420+
421+
expect(result).toBeInstanceOf(RetryableQuotaError);
422+
if (result instanceof RetryableQuotaError) {
423+
expect(result.retryDelayMs).toBe(5000);
424+
}
425+
});
426+
427+
it('should return RetryableQuotaError with 5s fallback for 429 with some detail', () => {
428+
const errorWithEmptyDetails = {
429+
error: {
430+
code: 429,
431+
message: 'A generic 429 error with no retry message.',
432+
details: [
433+
{
434+
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
435+
reason: 'QUOTA_EXCEEDED',
436+
domain: 'googleapis.com',
437+
metadata: {
438+
quota_limit: '',
439+
},
440+
},
441+
],
442+
},
443+
};
444+
445+
const result = classifyGoogleError(errorWithEmptyDetails);
446+
447+
expect(result).toBeInstanceOf(RetryableQuotaError);
448+
if (result instanceof RetryableQuotaError) {
449+
expect(result.retryDelayMs).toBe(5000);
450+
}
451+
});
392452
});

packages/core/src/utils/googleQuotaErrors.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import type {
1313
import { parseGoogleApiError } from './googleErrors.js';
1414
import { getErrorStatus, ModelNotFoundError } from './httpErrors.js';
1515

16+
const DEFAULT_RETRYABLE_DELAY_SECOND = 5;
17+
1618
/**
1719
* A non-retryable error indicating a hard quota limit has been reached (e.g., daily limit).
1820
*/
@@ -112,6 +114,18 @@ export function classifyGoogleError(error: unknown): unknown {
112114
retryDelaySeconds,
113115
);
114116
}
117+
} else if (status === 429) {
118+
// Fallback: If it is a 429 but doesn't have a specific "retry in" message,
119+
// assume it is a temporary rate limit and retry after 5 sec (same as DEFAULT_RETRY_OPTIONS).
120+
return new RetryableQuotaError(
121+
errorMessage,
122+
googleApiError ?? {
123+
code: 429,
124+
message: errorMessage,
125+
details: [],
126+
},
127+
DEFAULT_RETRYABLE_DELAY_SECOND,
128+
);
115129
}
116130

117131
return error; // Not a 429 error we can handle with structured details or a parsable retry message.
@@ -232,5 +246,21 @@ export function classifyGoogleError(error: unknown): unknown {
232246
);
233247
}
234248
}
249+
250+
// If we reached this point and the status is still 429, we return retryable.
251+
if (status === 429) {
252+
const errorMessage =
253+
googleApiError?.message ||
254+
(error instanceof Error ? error.message : String(error));
255+
return new RetryableQuotaError(
256+
errorMessage,
257+
googleApiError ?? {
258+
code: 429,
259+
message: errorMessage,
260+
details: [],
261+
},
262+
DEFAULT_RETRYABLE_DELAY_SECOND,
263+
);
264+
}
235265
return error; // Fallback to original error if no specific classification fits.
236266
}

0 commit comments

Comments
 (0)