Skip to content

Commit b11fab6

Browse files
JayadityaGitgemini-code-assist[bot]jacob314
authored andcommitted
feat: add Pro Quota Dialog (#7094)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Jacob Richman <[email protected]> (cherry picked from commit a63e678)
1 parent 1744fae commit b11fab6

File tree

3 files changed

+230
-8
lines changed

3 files changed

+230
-8
lines changed

packages/cli/src/ui/App.tsx

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import {
8282
isGenericQuotaExceededError,
8383
UserTierId,
8484
uiTelemetryService,
85+
DEFAULT_GEMINI_FLASH_MODEL,
8586
} from '@vybestack/llxprt-code-core';
8687
import {
8788
IdeIntegrationNudge,
@@ -122,6 +123,7 @@ import { ShowMoreLines } from './components/ShowMoreLines.js';
122123
import { PrivacyNotice } from './privacy/PrivacyNotice.js';
123124
import { useSettingsCommand } from './hooks/useSettingsCommand.js';
124125
import { SettingsDialog } from './components/SettingsDialog.js';
126+
import { ProQuotaDialog } from './components/ProQuotaDialog.js';
125127
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
126128
import { appEvents, AppEvent } from '../utils/events.js';
127129
import { getProviderManager } from '../providers/providerManagerInstance.js';
@@ -377,13 +379,22 @@ const App = (props: AppInternalProps) => {
377379
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
378380
const [isProcessing, setIsProcessing] = useState<boolean>(false);
379381
const [providerModels, setProviderModels] = useState<IModel[]>([]);
382+
380383
const {
381384
showWorkspaceMigrationDialog,
382385
workspaceExtensions,
383386
onWorkspaceMigrationDialogOpen,
384387
onWorkspaceMigrationDialogClose,
385388
} = useWorkspaceMigration(settings);
386389

390+
const [isProQuotaDialogOpen, setIsProQuotaDialogOpen] = useState(false);
391+
const [proQuotaDialogResolver, setProQuotaDialogResolver] = useState<
392+
((value: boolean) => void) | null
393+
>(null);
394+
const [proQuotaDialogResolver, setProQuotaDialogResolver] = useState<
395+
((value: boolean) => void) | null
396+
>(null);
397+
387398
useEffect(() => {
388399
const unsubscribe = ideContext.subscribeToIdeContext(setIdeContextState);
389400
// Set the initial value
@@ -752,6 +763,12 @@ const App = (props: AppInternalProps) => {
752763
fallbackModel: string,
753764
error?: unknown,
754765
): Promise<boolean> => {
766+
// Check if we've already switched to the fallback model
767+
if (config.isInFallbackMode()) {
768+
// If we're already in fallback mode, don't show the dialog again
769+
return false;
770+
}
771+
755772
let message: string;
756773

757774
const contentGenConfig = config.getContentGeneratorConfig();
@@ -764,14 +781,15 @@ const App = (props: AppInternalProps) => {
764781
// Check if this is a Pro quota exceeded error
765782
if (error && isProQuotaExceededError(error)) {
766783
if (isPaidTier) {
767-
message = `You have reached your daily ${currentModel} quota limit.
768-
To continue using ${currentModel}, you can use /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey
769-
Or you can switch to a different model using the /model command`;
784+
message = `You have reached your daily ${currentModel} quota limit.
785+
⚡ You can choose to authenticate with a paid API key or continue with the fallback model.
786+
⚡ To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
770787
} else {
771-
message = `You have reached your daily ${currentModel} quota limit.
772-
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
773-
Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key
774-
You can switch authentication methods by typing /auth or switch to a different model using /model`;
788+
message = `⚡ You have reached your daily ${currentModel} quota limit.
789+
⚡ You can choose to authenticate with a paid API key or continue with the fallback model.
790+
⚡ 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
791+
⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key
792+
⚡ You can switch authentication methods by typing /auth`;
775793
}
776794
} else if (error && isGenericQuotaExceededError(error)) {
777795
if (isPaidTier) {
@@ -812,6 +830,40 @@ You can switch authentication methods by typing /auth or switch to a different m
812830
);
813831
}
814832

833+
// For Pro quota errors, show the dialog and wait for user's choice
834+
if (error && isProQuotaExceededError(error)) {
835+
// Set the flag to prevent tool continuation
836+
setModelSwitchedFromQuotaError(true);
837+
// Set global quota error flag to prevent Flash model calls
838+
config.setQuotaErrorOccurred(true);
839+
840+
// Show the ProQuotaDialog and wait for user's choice
841+
const shouldContinueWithFallback = await new Promise<boolean>(
842+
(resolve) => {
843+
setIsProQuotaDialogOpen(true);
844+
setProQuotaDialogResolver(() => resolve);
845+
},
846+
);
847+
848+
// If user chose to continue with fallback, we don't need to stop the current prompt
849+
if (shouldContinueWithFallback) {
850+
// Switch to fallback model for future use
851+
config.setModel(fallbackModel);
852+
config.setFallbackMode(true);
853+
logFlashFallback(
854+
config,
855+
new FlashFallbackEvent(
856+
config.getContentGeneratorConfig().authType!,
857+
),
858+
);
859+
return true; // Continue with current prompt using fallback model
860+
}
861+
862+
// If user chose to authenticate, stop current prompt
863+
return false;
864+
}
865+
866+
// For other quota errors, automatically switch to fallback model
815867
// Set the flag to prevent tool continuation
816868
setModelSwitchedFromQuotaError(true);
817869
// Set global quota error flag to prevent Flash model calls
@@ -1223,7 +1275,11 @@ You can switch authentication methods by typing /auth or switch to a different m
12231275
}, [history, logger]);
12241276

12251277
const isInputActive =
1226-
streamingState === StreamingState.Idle && !initError && !isProcessing;
1278+
(streamingState === StreamingState.Idle ||
1279+
streamingState === StreamingState.Responding) &&
1280+
!initError &&
1281+
!isProcessing &&
1282+
!isProQuotaDialogOpen;
12271283

12281284
const handleClearScreen = useCallback(() => {
12291285
clearItems();
@@ -1469,6 +1525,31 @@ You can switch authentication methods by typing /auth or switch to a different m
14691525
ide={currentIDE}
14701526
onComplete={handleIdePromptComplete}
14711527
/>
1528+
) : isProQuotaDialogOpen ? (
1529+
<ProQuotaDialog
1530+
currentModel={config.getModel()}
1531+
fallbackModel={DEFAULT_GEMINI_FLASH_MODEL}
1532+
onChoice={(choice) => {
1533+
setIsProQuotaDialogOpen(false);
1534+
if (!proQuotaDialogResolver) return;
1535+
1536+
const resolveValue = choice !== 'auth';
1537+
proQuotaDialogResolver(resolveValue);
1538+
setProQuotaDialogResolver(null);
1539+
1540+
if (choice === 'auth') {
1541+
openAuthDialog();
1542+
} else {
1543+
addItem(
1544+
{
1545+
type: MessageType.INFO,
1546+
text: 'Switched to fallback model. Tip: Press Ctrl+P to recall your previous prompt and submit it again if you wish.',
1547+
},
1548+
Date.now(),
1549+
);
1550+
}
1551+
}}
1552+
/>
14721553
) : isFolderTrustDialogOpen ? (
14731554
<FolderTrustDialog
14741555
onSelect={handleFolderTrustSelect}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { render } from 'ink-testing-library';
8+
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
9+
import { ProQuotaDialog } from './ProQuotaDialog.js';
10+
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
11+
12+
// Mock the child component to make it easier to test the parent
13+
vi.mock('./shared/RadioButtonSelect.js', () => ({
14+
RadioButtonSelect: vi.fn(),
15+
}));
16+
17+
describe('ProQuotaDialog', () => {
18+
beforeEach(() => {
19+
vi.clearAllMocks();
20+
});
21+
22+
it('should render with correct title and options', () => {
23+
const { lastFrame } = render(
24+
<ProQuotaDialog
25+
currentModel="gemini-2.5-pro"
26+
fallbackModel="gemini-2.5-flash"
27+
onChoice={() => {}}
28+
/>,
29+
);
30+
31+
const output = lastFrame();
32+
expect(output).toContain('Pro quota limit reached for gemini-2.5-pro.');
33+
34+
// Check that RadioButtonSelect was called with the correct items
35+
expect(RadioButtonSelect).toHaveBeenCalledWith(
36+
expect.objectContaining({
37+
items: [
38+
{
39+
label: 'Change auth (executes the /auth command)',
40+
value: 'auth',
41+
},
42+
{
43+
label: `Continue with gemini-2.5-flash`,
44+
value: 'continue',
45+
},
46+
],
47+
}),
48+
undefined,
49+
);
50+
});
51+
52+
it('should call onChoice with "auth" when "Change auth" is selected', () => {
53+
const mockOnChoice = vi.fn();
54+
render(
55+
<ProQuotaDialog
56+
currentModel="gemini-2.5-pro"
57+
fallbackModel="gemini-2.5-flash"
58+
onChoice={mockOnChoice}
59+
/>,
60+
);
61+
62+
// Get the onSelect function passed to RadioButtonSelect
63+
const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect;
64+
65+
// Simulate the selection
66+
onSelect('auth');
67+
68+
expect(mockOnChoice).toHaveBeenCalledWith('auth');
69+
});
70+
71+
it('should call onChoice with "continue" when "Continue with flash" is selected', () => {
72+
const mockOnChoice = vi.fn();
73+
render(
74+
<ProQuotaDialog
75+
currentModel="gemini-2.5-pro"
76+
fallbackModel="gemini-2.5-flash"
77+
onChoice={mockOnChoice}
78+
/>,
79+
);
80+
81+
// Get the onSelect function passed to RadioButtonSelect
82+
const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect;
83+
84+
// Simulate the selection
85+
onSelect('continue');
86+
87+
expect(mockOnChoice).toHaveBeenCalledWith('continue');
88+
});
89+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import type React from 'react';
8+
import { Box, Text } from 'ink';
9+
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
10+
import { Colors } from '../colors.js';
11+
12+
interface ProQuotaDialogProps {
13+
currentModel: string;
14+
fallbackModel: string;
15+
onChoice: (choice: 'auth' | 'continue') => void;
16+
}
17+
18+
export function ProQuotaDialog({
19+
currentModel,
20+
fallbackModel,
21+
onChoice,
22+
}: ProQuotaDialogProps): React.JSX.Element {
23+
const items = [
24+
{
25+
label: 'Change auth (executes the /auth command)',
26+
value: 'auth' as const,
27+
},
28+
{
29+
label: `Continue with ${fallbackModel}`,
30+
value: 'continue' as const,
31+
},
32+
];
33+
34+
const handleSelect = (choice: 'auth' | 'continue') => {
35+
onChoice(choice);
36+
};
37+
38+
return (
39+
<Box borderStyle="round" flexDirection="column" paddingX={1}>
40+
<Text bold color={Colors.AccentYellow}>
41+
Pro quota limit reached for {currentModel}.
42+
</Text>
43+
<Box marginTop={1}>
44+
<RadioButtonSelect
45+
items={items}
46+
initialIndex={1}
47+
onSelect={handleSelect}
48+
/>
49+
</Box>
50+
</Box>
51+
);
52+
}

0 commit comments

Comments
 (0)