Skip to content

Commit d9f6ceb

Browse files
fix(patch): cherry-pick ee92db7 to release/v0.11.0-pr-11624 to patch version v0.11.0 and create version 0.11.1 (#12321)
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
1 parent 92f5355 commit d9f6ceb

14 files changed

Lines changed: 1357 additions & 804 deletions

packages/cli/src/ui/hooks/useQuotaAndFallback.test.ts

Lines changed: 46 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,15 @@ import {
1919
type FallbackModelHandler,
2020
UserTierId,
2121
AuthType,
22-
isGenericQuotaExceededError,
23-
isProQuotaExceededError,
22+
TerminalQuotaError,
2423
makeFakeConfig,
24+
type GoogleApiError,
25+
RetryableQuotaError,
2526
} from '@google/gemini-cli-core';
2627
import { useQuotaAndFallback } from './useQuotaAndFallback.js';
2728
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
2829
import { AuthState, MessageType } from '../types.js';
2930

30-
// Mock the error checking functions from the core package to control test scenarios
31-
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
32-
const original =
33-
await importOriginal<typeof import('@google/gemini-cli-core')>();
34-
return {
35-
...original,
36-
isGenericQuotaExceededError: vi.fn(),
37-
isProQuotaExceededError: vi.fn(),
38-
};
39-
});
40-
4131
// Use a type alias for SpyInstance as it's not directly exported
4232
type SpyInstance = ReturnType<typeof vi.spyOn>;
4333

@@ -47,12 +37,15 @@ describe('useQuotaAndFallback', () => {
4737
let mockSetAuthState: Mock;
4838
let mockSetModelSwitchedFromQuotaError: Mock;
4939
let setFallbackHandlerSpy: SpyInstance;
50-
51-
const mockedIsGenericQuotaExceededError = isGenericQuotaExceededError as Mock;
52-
const mockedIsProQuotaExceededError = isProQuotaExceededError as Mock;
40+
let mockGoogleApiError: GoogleApiError;
5341

5442
beforeEach(() => {
5543
mockConfig = makeFakeConfig();
44+
mockGoogleApiError = {
45+
code: 429,
46+
message: 'mock error',
47+
details: [],
48+
};
5649

5750
// Spy on the method that requires the private field and mock its return.
5851
// This is cleaner than modifying the config class for tests.
@@ -72,9 +65,6 @@ describe('useQuotaAndFallback', () => {
7265

7366
setFallbackHandlerSpy = vi.spyOn(mockConfig, 'setFallbackModelHandler');
7467
vi.spyOn(mockConfig, 'setQuotaErrorOccurred');
75-
76-
mockedIsGenericQuotaExceededError.mockReturnValue(false);
77-
mockedIsProQuotaExceededError.mockReturnValue(false);
7868
});
7969

8070
afterEach(() => {
@@ -140,51 +130,62 @@ describe('useQuotaAndFallback', () => {
140130
describe('Automatic Fallback Scenarios', () => {
141131
const testCases = [
142132
{
143-
errorType: 'generic',
133+
description: 'other error for FREE tier',
144134
tier: UserTierId.FREE,
135+
error: new Error('some error'),
145136
expectedMessageSnippets: [
146-
'Automatically switching from model-A to model-B',
137+
'Automatically switching from model-A to model-B for faster responses',
147138
'upgrade to a Gemini Code Assist Standard or Enterprise plan',
148139
],
149140
},
150141
{
151-
errorType: 'generic',
152-
tier: UserTierId.STANDARD, // Paid tier
142+
description: 'other error for LEGACY tier',
143+
tier: UserTierId.LEGACY, // Paid tier
144+
error: new Error('some error'),
153145
expectedMessageSnippets: [
154-
'Automatically switching from model-A to model-B',
146+
'Automatically switching from model-A to model-B for faster responses',
155147
'switch to using a paid API key from AI Studio',
156148
],
157149
},
158150
{
159-
errorType: 'other',
151+
description: 'retryable quota error for FREE tier',
160152
tier: UserTierId.FREE,
153+
error: new RetryableQuotaError(
154+
'retryable quota',
155+
mockGoogleApiError,
156+
5,
157+
),
161158
expectedMessageSnippets: [
162-
'Automatically switching from model-A to model-B for faster responses',
163-
'upgrade to a Gemini Code Assist Standard or Enterprise plan',
159+
'Your requests are being throttled right now due to server being at capacity for model-A',
160+
'Automatically switching from model-A to model-B',
161+
'upgrading to a Gemini Code Assist Standard or Enterprise plan',
164162
],
165163
},
166164
{
167-
errorType: 'other',
165+
description: 'retryable quota error for LEGACY tier',
168166
tier: UserTierId.LEGACY, // Paid tier
167+
error: new RetryableQuotaError(
168+
'retryable quota',
169+
mockGoogleApiError,
170+
5,
171+
),
169172
expectedMessageSnippets: [
170-
'Automatically switching from model-A to model-B for faster responses',
173+
'Your requests are being throttled right now due to server being at capacity for model-A',
174+
'Automatically switching from model-A to model-B',
171175
'switch to using a paid API key from AI Studio',
172176
],
173177
},
174178
];
175179

176-
for (const { errorType, tier, expectedMessageSnippets } of testCases) {
177-
it(`should handle ${errorType} error for ${tier} tier correctly`, async () => {
178-
mockedIsGenericQuotaExceededError.mockReturnValue(
179-
errorType === 'generic',
180-
);
181-
180+
for (const {
181+
description,
182+
tier,
183+
error,
184+
expectedMessageSnippets,
185+
} of testCases) {
186+
it(`should handle ${description} correctly`, async () => {
182187
const handler = getRegisteredHandler(tier);
183-
const result = await handler(
184-
'model-A',
185-
'model-B',
186-
new Error('quota exceeded'),
187-
);
188+
const result = await handler('model-A', 'model-B', error);
188189

189190
// Automatic fallbacks should return 'stop'
190191
expect(result).toBe('stop');
@@ -207,10 +208,6 @@ describe('useQuotaAndFallback', () => {
207208
});
208209

209210
describe('Interactive Fallback (Pro Quota Error)', () => {
210-
beforeEach(() => {
211-
mockedIsProQuotaExceededError.mockReturnValue(true);
212-
});
213-
214211
it('should set an interactive request and wait for user choice', async () => {
215212
const { result } = renderHook(() =>
216213
useQuotaAndFallback({
@@ -229,7 +226,7 @@ describe('useQuotaAndFallback', () => {
229226
const promise = handler(
230227
'gemini-pro',
231228
'gemini-flash',
232-
new Error('pro quota'),
229+
new TerminalQuotaError('pro quota', mockGoogleApiError),
233230
);
234231

235232
await act(async () => {});
@@ -268,7 +265,7 @@ describe('useQuotaAndFallback', () => {
268265
const promise1 = handler(
269266
'gemini-pro',
270267
'gemini-flash',
271-
new Error('pro quota 1'),
268+
new TerminalQuotaError('pro quota 1', mockGoogleApiError),
272269
);
273270
await act(async () => {});
274271

@@ -278,7 +275,7 @@ describe('useQuotaAndFallback', () => {
278275
const result2 = await handler(
279276
'gemini-pro',
280277
'gemini-flash',
281-
new Error('pro quota 2'),
278+
new TerminalQuotaError('pro quota 2', mockGoogleApiError),
282279
);
283280

284281
// The lock should have stopped the second request
@@ -297,10 +294,6 @@ describe('useQuotaAndFallback', () => {
297294
});
298295

299296
describe('handleProQuotaChoice', () => {
300-
beforeEach(() => {
301-
mockedIsProQuotaExceededError.mockReturnValue(true);
302-
});
303-
304297
it('should do nothing if there is no pending pro quota request', () => {
305298
const { result } = renderHook(() =>
306299
useQuotaAndFallback({
@@ -336,7 +329,7 @@ describe('useQuotaAndFallback', () => {
336329
const promise = handler(
337330
'gemini-pro',
338331
'gemini-flash',
339-
new Error('pro quota'),
332+
new TerminalQuotaError('pro quota', mockGoogleApiError),
340333
);
341334
await act(async () => {}); // Allow state to update
342335

@@ -367,7 +360,7 @@ describe('useQuotaAndFallback', () => {
367360
const promise = handler(
368361
'gemini-pro',
369362
'gemini-flash',
370-
new Error('pro quota'),
363+
new TerminalQuotaError('pro quota', mockGoogleApiError),
371364
);
372365
await act(async () => {}); // Allow state to update
373366

packages/cli/src/ui/hooks/useQuotaAndFallback.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
type Config,
1010
type FallbackModelHandler,
1111
type FallbackIntent,
12-
isGenericQuotaExceededError,
13-
isProQuotaExceededError,
12+
TerminalQuotaError,
1413
UserTierId,
14+
RetryableQuotaError,
1515
} from '@google/gemini-cli-core';
1616
import { useCallback, useEffect, useRef, useState } from 'react';
1717
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
@@ -63,7 +63,7 @@ export function useQuotaAndFallback({
6363

6464
let message: string;
6565

66-
if (error && isProQuotaExceededError(error)) {
66+
if (error instanceof TerminalQuotaError) {
6767
// Pro Quota specific messages (Interactive)
6868
if (isPaidTier) {
6969
message = `⚡ You have reached your daily ${failedModel} quota limit.
@@ -76,31 +76,30 @@ export function useQuotaAndFallback({
7676
⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key
7777
⚡ You can switch authentication methods by typing /auth`;
7878
}
79-
} else if (error && isGenericQuotaExceededError(error)) {
80-
// Generic Quota (Automatic fallback)
81-
const actionMessage = `⚡ You have reached your daily quota limit.\n⚡ Automatically switching from ${failedModel} to ${fallbackModel} for the remainder of this session.`;
79+
} else if (error instanceof RetryableQuotaError) {
80+
// Short term quota retries exhausted (Automatic fallback)
81+
const actionMessage = `⚡ Your requests are being throttled right now due to server being at capacity for ${failedModel}.\n⚡ Automatically switching from ${failedModel} to ${fallbackModel} for the remainder of this session.`;
8282

8383
if (isPaidTier) {
8484
message = `${actionMessage}
85-
⚡ To continue accessing the ${failedModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
85+
⚡ To continue accessing the ${failedModel} model, retry your request after some time or consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
8686
} else {
8787
message = `${actionMessage}
88-
⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist
89-
⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key
88+
⚡ Retry your requests after some time. Otherwise consider upgrading to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist
9089
⚡ You can switch authentication methods by typing /auth`;
9190
}
9291
} else {
93-
// Consecutive 429s or other errors (Automatic fallback)
92+
// Other errors (Automatic fallback)
9493
const actionMessage = `⚡ Automatically switching from ${failedModel} to ${fallbackModel} for faster responses for the remainder of this session.`;
9594

9695
if (isPaidTier) {
9796
message = `${actionMessage}
98-
Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${failedModel} quota limit
99-
⚡ To continue accessing the ${failedModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
97+
Your requests are being throttled temporarily due to server being at capacity for ${failedModel} or there is a service outage.
98+
⚡ To continue accessing the ${failedModel} model, you can retry your request after some time or consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
10099
} else {
101100
message = `${actionMessage}
102-
Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${failedModel} quota limit
103-
⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist
101+
Your requests are being throttled temporarily due to server being at capacity for ${failedModel} or there is a service outage.
102+
⚡ To avoid being throttled, you can retry your request after some time or upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist
104103
⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key
105104
⚡ You can switch authentication methods by typing /auth`;
106105
}
@@ -119,7 +118,7 @@ export function useQuotaAndFallback({
119118
config.setQuotaErrorOccurred(true);
120119

121120
// Interactive Fallback for Pro quota
122-
if (error && isProQuotaExceededError(error)) {
121+
if (error instanceof TerminalQuotaError) {
123122
if (isDialogPending.current) {
124123
return 'stop'; // A dialog is already active, so just stop this request.
125124
}

packages/core/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ export { makeFakeConfig } from './src/test-utils/config.js';
4444
export * from './src/utils/pathReader.js';
4545
export { ClearcutLogger } from './src/telemetry/clearcut-logger/clearcut-logger.js';
4646
export { logModelSlashCommand } from './src/telemetry/loggers.js';
47+
export * from './src/utils/googleQuotaErrors.js';
48+
export type { GoogleApiError } from './src/utils/googleErrors.js';

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export * from './utils/gitIgnoreParser.js';
4545
export * from './utils/gitUtils.js';
4646
export * from './utils/editor.js';
4747
export * from './utils/quotaErrorDetection.js';
48+
export * from './utils/googleQuotaErrors.js';
4849
export * from './utils/fileUtils.js';
4950
export * from './utils/retry.js';
5051
export * from './utils/shell-utils.js';

0 commit comments

Comments
 (0)