Skip to content

Commit f51d745

Browse files
authored
refactor: parse string for retryInfo (#12586)
1 parent c743631 commit f51d745

File tree

2 files changed

+66
-5
lines changed

2 files changed

+66
-5
lines changed

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,44 @@ describe('classifyGoogleError', () => {
2525
expect(result).toBe(regularError);
2626
});
2727

28+
it('should return RetryableQuotaError when message contains "Please retry in Xs"', () => {
29+
const complexError = {
30+
error: {
31+
message:
32+
'{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 44.097740004s."}]}}',
33+
code: 429,
34+
status: 'Too Many Requests',
35+
},
36+
};
37+
const rawError = new Error(JSON.stringify(complexError));
38+
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
39+
40+
const result = classifyGoogleError(rawError);
41+
42+
expect(result).toBeInstanceOf(RetryableQuotaError);
43+
expect((result as RetryableQuotaError).retryDelayMs).toBe(44097.740004);
44+
expect((result as RetryableQuotaError).message).toBe(rawError.message);
45+
});
46+
47+
it('should return RetryableQuotaError when error is a string and message contains "Please retry in Xms"', () => {
48+
const complexErrorString = JSON.stringify({
49+
error: {
50+
message:
51+
'{"error": {"code": 429, "status": 429, "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms.", "details": [{"detail": "??? to (unknown) : APP_ERROR(8) You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/usage?tab=rate-limit. \n* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 2\nPlease retry in 900.2ms."}]}}',
52+
code: 429,
53+
status: 'Too Many Requests',
54+
},
55+
});
56+
const rawError = new Error(complexErrorString);
57+
vi.spyOn(errorParser, 'parseGoogleApiError').mockReturnValue(null);
58+
59+
const result = classifyGoogleError(rawError);
60+
61+
expect(result).toBeInstanceOf(RetryableQuotaError);
62+
expect((result as RetryableQuotaError).retryDelayMs).toBeCloseTo(900.2);
63+
expect((result as RetryableQuotaError).message).toBe(rawError.message);
64+
});
65+
2866
it('should return original error if code is not 429', () => {
2967
const apiError: GoogleApiError = {
3068
code: 500,

packages/core/src/utils/googleQuotaErrors.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,20 @@ export class RetryableQuotaError extends Error {
4343
}
4444

4545
/**
46-
* Parses a duration string (e.g., "34.074824224s", "60s") and returns the time in seconds.
46+
* Parses a duration string (e.g., "34.074824224s", "60s", "900ms") and returns the time in seconds.
4747
* @param duration The duration string to parse.
4848
* @returns The duration in seconds, or null if parsing fails.
4949
*/
5050
function parseDurationInSeconds(duration: string): number | null {
51-
if (!duration.endsWith('s')) {
52-
return null;
51+
if (duration.endsWith('ms')) {
52+
const milliseconds = parseFloat(duration.slice(0, -2));
53+
return isNaN(milliseconds) ? null : milliseconds / 1000;
5354
}
54-
const seconds = parseFloat(duration.slice(0, -1));
55-
return isNaN(seconds) ? null : seconds;
55+
if (duration.endsWith('s')) {
56+
const seconds = parseFloat(duration.slice(0, -1));
57+
return isNaN(seconds) ? null : seconds;
58+
}
59+
return null;
5660
}
5761

5862
/**
@@ -64,6 +68,7 @@ function parseDurationInSeconds(duration: string): number | null {
6468
* - If the error suggests a retry delay of more than 2 minutes, it's a `TerminalQuotaError`.
6569
* - If the error suggests a retry delay of 2 minutes or less, it's a `RetryableQuotaError`.
6670
* - If the error indicates a per-minute limit, it's a `RetryableQuotaError`.
71+
* - If the error message contains the phrase "Please retry in X[s|ms]", it's a `RetryableQuotaError`.
6772
*
6873
* @param error The error to classify.
6974
* @returns A `TerminalQuotaError`, `RetryableQuotaError`, or the original `unknown` error.
@@ -72,6 +77,24 @@ export function classifyGoogleError(error: unknown): unknown {
7277
const googleApiError = parseGoogleApiError(error);
7378

7479
if (!googleApiError || googleApiError.code !== 429) {
80+
// Fallback: try to parse the error message for a retry delay
81+
const errorMessage = error instanceof Error ? error.message : String(error);
82+
const match = errorMessage.match(/Please retry in ([0-9.]+(?:ms|s))/);
83+
if (match?.[1]) {
84+
const retryDelaySeconds = parseDurationInSeconds(match[1]);
85+
if (retryDelaySeconds !== null) {
86+
return new RetryableQuotaError(
87+
errorMessage,
88+
googleApiError ?? {
89+
code: 429,
90+
message: errorMessage,
91+
details: [],
92+
},
93+
retryDelaySeconds,
94+
);
95+
}
96+
}
97+
7598
return error; // Not a 429 error we can handle.
7699
}
77100

0 commit comments

Comments
 (0)