Skip to content

Commit 319f43f

Browse files
authored
fix: handle request retries and model fallback correctly (google-gemini#9407)
1 parent f2308db commit 319f43f

13 files changed

+999
-807
lines changed

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

Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,14 @@ import {
1919
type FallbackModelHandler,
2020
UserTierId,
2121
AuthType,
22-
isGenericQuotaExceededError,
23-
isProQuotaExceededError,
22+
TerminalQuotaError,
2423
makeFakeConfig,
24+
type GoogleApiError,
2525
} from '@google/gemini-cli-core';
2626
import { useQuotaAndFallback } from './useQuotaAndFallback.js';
2727
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
2828
import { AuthState, MessageType } from '../types.js';
2929

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-
4130
// Use a type alias for SpyInstance as it's not directly exported
4231
type SpyInstance = ReturnType<typeof vi.spyOn>;
4332

@@ -47,12 +36,15 @@ describe('useQuotaAndFallback', () => {
4736
let mockSetAuthState: Mock;
4837
let mockSetModelSwitchedFromQuotaError: Mock;
4938
let setFallbackHandlerSpy: SpyInstance;
50-
51-
const mockedIsGenericQuotaExceededError = isGenericQuotaExceededError as Mock;
52-
const mockedIsProQuotaExceededError = isProQuotaExceededError as Mock;
39+
let mockGoogleApiError: GoogleApiError;
5340

5441
beforeEach(() => {
5542
mockConfig = makeFakeConfig();
43+
mockGoogleApiError = {
44+
code: 429,
45+
message: 'mock error',
46+
details: [],
47+
};
5648

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

7365
setFallbackHandlerSpy = vi.spyOn(mockConfig, 'setFallbackModelHandler');
7466
vi.spyOn(mockConfig, 'setQuotaErrorOccurred');
75-
76-
mockedIsGenericQuotaExceededError.mockReturnValue(false);
77-
mockedIsProQuotaExceededError.mockReturnValue(false);
7867
});
7968

8069
afterEach(() => {
@@ -139,22 +128,6 @@ describe('useQuotaAndFallback', () => {
139128

140129
describe('Automatic Fallback Scenarios', () => {
141130
const testCases = [
142-
{
143-
errorType: 'generic',
144-
tier: UserTierId.FREE,
145-
expectedMessageSnippets: [
146-
'Automatically switching from model-A to model-B',
147-
'upgrade to a Gemini Code Assist Standard or Enterprise plan',
148-
],
149-
},
150-
{
151-
errorType: 'generic',
152-
tier: UserTierId.STANDARD, // Paid tier
153-
expectedMessageSnippets: [
154-
'Automatically switching from model-A to model-B',
155-
'switch to using a paid API key from AI Studio',
156-
],
157-
},
158131
{
159132
errorType: 'other',
160133
tier: UserTierId.FREE,
@@ -175,15 +148,11 @@ describe('useQuotaAndFallback', () => {
175148

176149
for (const { errorType, tier, expectedMessageSnippets } of testCases) {
177150
it(`should handle ${errorType} error for ${tier} tier correctly`, async () => {
178-
mockedIsGenericQuotaExceededError.mockReturnValue(
179-
errorType === 'generic',
180-
);
181-
182151
const handler = getRegisteredHandler(tier);
183152
const result = await handler(
184153
'model-A',
185154
'model-B',
186-
new Error('quota exceeded'),
155+
new Error('some error'),
187156
);
188157

189158
// Automatic fallbacks should return 'stop'
@@ -207,10 +176,6 @@ describe('useQuotaAndFallback', () => {
207176
});
208177

209178
describe('Interactive Fallback (Pro Quota Error)', () => {
210-
beforeEach(() => {
211-
mockedIsProQuotaExceededError.mockReturnValue(true);
212-
});
213-
214179
it('should set an interactive request and wait for user choice', async () => {
215180
const { result } = renderHook(() =>
216181
useQuotaAndFallback({
@@ -229,7 +194,7 @@ describe('useQuotaAndFallback', () => {
229194
const promise = handler(
230195
'gemini-pro',
231196
'gemini-flash',
232-
new Error('pro quota'),
197+
new TerminalQuotaError('pro quota', mockGoogleApiError),
233198
);
234199

235200
await act(async () => {});
@@ -268,7 +233,7 @@ describe('useQuotaAndFallback', () => {
268233
const promise1 = handler(
269234
'gemini-pro',
270235
'gemini-flash',
271-
new Error('pro quota 1'),
236+
new TerminalQuotaError('pro quota 1', mockGoogleApiError),
272237
);
273238
await act(async () => {});
274239

@@ -278,7 +243,7 @@ describe('useQuotaAndFallback', () => {
278243
const result2 = await handler(
279244
'gemini-pro',
280245
'gemini-flash',
281-
new Error('pro quota 2'),
246+
new TerminalQuotaError('pro quota 2', mockGoogleApiError),
282247
);
283248

284249
// The lock should have stopped the second request
@@ -297,10 +262,6 @@ describe('useQuotaAndFallback', () => {
297262
});
298263

299264
describe('handleProQuotaChoice', () => {
300-
beforeEach(() => {
301-
mockedIsProQuotaExceededError.mockReturnValue(true);
302-
});
303-
304265
it('should do nothing if there is no pending pro quota request', () => {
305266
const { result } = renderHook(() =>
306267
useQuotaAndFallback({
@@ -336,7 +297,7 @@ describe('useQuotaAndFallback', () => {
336297
const promise = handler(
337298
'gemini-pro',
338299
'gemini-flash',
339-
new Error('pro quota'),
300+
new TerminalQuotaError('pro quota', mockGoogleApiError),
340301
);
341302
await act(async () => {}); // Allow state to update
342303

@@ -367,7 +328,7 @@ describe('useQuotaAndFallback', () => {
367328
const promise = handler(
368329
'gemini-pro',
369330
'gemini-flash',
370-
new Error('pro quota'),
331+
new TerminalQuotaError('pro quota', mockGoogleApiError),
371332
);
372333
await act(async () => {}); // Allow state to update
373334

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

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import {
99
type Config,
1010
type FallbackModelHandler,
1111
type FallbackIntent,
12-
isGenericQuotaExceededError,
13-
isProQuotaExceededError,
12+
TerminalQuotaError,
1413
UserTierId,
1514
} from '@google/gemini-cli-core';
1615
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -63,7 +62,7 @@ export function useQuotaAndFallback({
6362

6463
let message: string;
6564

66-
if (error && isProQuotaExceededError(error)) {
65+
if (error instanceof TerminalQuotaError) {
6766
// Pro Quota specific messages (Interactive)
6867
if (isPaidTier) {
6968
message = `⚡ You have reached your daily ${failedModel} quota limit.
@@ -74,19 +73,6 @@ export function useQuotaAndFallback({
7473
⚡ You can choose to authenticate with a paid API key or continue with the fallback model.
7574
⚡ 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
7675
⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key
77-
⚡ You can switch authentication methods by typing /auth`;
78-
}
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.`;
82-
83-
if (isPaidTier) {
84-
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`;
86-
} else {
87-
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
9076
⚡ You can switch authentication methods by typing /auth`;
9177
}
9278
} else {
@@ -119,7 +105,7 @@ export function useQuotaAndFallback({
119105
config.setQuotaErrorOccurred(true);
120106

121107
// Interactive Fallback for Pro quota
122-
if (error && isProQuotaExceededError(error)) {
108+
if (error instanceof TerminalQuotaError) {
123109
if (isDialogPending.current) {
124110
return 'stop'; // A dialog is already active, so just stop this request.
125111
}

packages/core/index.ts

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

0 commit comments

Comments
 (0)