2025-09-08 16:19:52 -04:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
AuthType,
|
|
|
|
|
type Config,
|
|
|
|
|
type FallbackModelHandler,
|
|
|
|
|
type FallbackIntent,
|
2025-10-24 11:09:06 -07:00
|
|
|
TerminalQuotaError,
|
2025-11-18 12:01:16 -05:00
|
|
|
ModelNotFoundError,
|
|
|
|
|
type UserTierId,
|
|
|
|
|
PREVIEW_GEMINI_MODEL,
|
2025-11-25 16:17:22 -05:00
|
|
|
DEFAULT_GEMINI_MODEL,
|
2025-12-17 09:43:21 -08:00
|
|
|
VALID_GEMINI_MODELS,
|
2025-09-08 16:19:52 -04:00
|
|
|
} from '@google/gemini-cli-core';
|
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
|
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
2025-11-06 13:43:21 -05:00
|
|
|
import { MessageType } from '../types.js';
|
2025-09-08 16:19:52 -04:00
|
|
|
import { type ProQuotaDialogRequest } from '../contexts/UIStateContext.js';
|
|
|
|
|
|
|
|
|
|
interface UseQuotaAndFallbackArgs {
|
|
|
|
|
config: Config;
|
|
|
|
|
historyManager: UseHistoryManagerReturn;
|
|
|
|
|
userTier: UserTierId | undefined;
|
|
|
|
|
setModelSwitchedFromQuotaError: (value: boolean) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useQuotaAndFallback({
|
|
|
|
|
config,
|
|
|
|
|
historyManager,
|
|
|
|
|
userTier,
|
|
|
|
|
setModelSwitchedFromQuotaError,
|
|
|
|
|
}: UseQuotaAndFallbackArgs) {
|
|
|
|
|
const [proQuotaRequest, setProQuotaRequest] =
|
|
|
|
|
useState<ProQuotaDialogRequest | null>(null);
|
|
|
|
|
const isDialogPending = useRef(false);
|
|
|
|
|
|
|
|
|
|
// Set up Flash fallback handler
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const fallbackHandler: FallbackModelHandler = async (
|
|
|
|
|
failedModel,
|
|
|
|
|
fallbackModel,
|
|
|
|
|
error,
|
|
|
|
|
): Promise<FallbackIntent | null> => {
|
|
|
|
|
// Fallbacks are currently only handled for OAuth users.
|
|
|
|
|
const contentGeneratorConfig = config.getContentGeneratorConfig();
|
|
|
|
|
if (
|
|
|
|
|
!contentGeneratorConfig ||
|
|
|
|
|
contentGeneratorConfig.authType !== AuthType.LOGIN_WITH_GOOGLE
|
|
|
|
|
) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let message: string;
|
2025-11-18 12:01:16 -05:00
|
|
|
let isTerminalQuotaError = false;
|
|
|
|
|
let isModelNotFoundError = false;
|
2025-11-25 16:17:22 -05:00
|
|
|
const usageLimitReachedModel =
|
|
|
|
|
failedModel === DEFAULT_GEMINI_MODEL ||
|
|
|
|
|
failedModel === PREVIEW_GEMINI_MODEL
|
|
|
|
|
? 'all Pro models'
|
|
|
|
|
: failedModel;
|
2025-10-24 11:09:06 -07:00
|
|
|
if (error instanceof TerminalQuotaError) {
|
2025-11-18 12:01:16 -05:00
|
|
|
isTerminalQuotaError = true;
|
2025-11-06 17:13:38 -05:00
|
|
|
// Common part of the message for both tiers
|
|
|
|
|
const messageLines = [
|
2025-11-25 16:17:22 -05:00
|
|
|
`Usage limit reached for ${usageLimitReachedModel}.`,
|
2025-11-18 12:01:16 -05:00
|
|
|
error.retryDelayMs ? getResetTimeMessage(error.retryDelayMs) : null,
|
|
|
|
|
`/stats for usage details`,
|
2025-12-17 09:43:21 -08:00
|
|
|
`/model to switch models.`,
|
2025-11-18 12:01:16 -05:00
|
|
|
`/auth to switch to API key.`,
|
|
|
|
|
].filter(Boolean);
|
|
|
|
|
message = messageLines.join('\n');
|
2025-12-17 09:43:21 -08:00
|
|
|
} else if (
|
|
|
|
|
error instanceof ModelNotFoundError &&
|
|
|
|
|
VALID_GEMINI_MODELS.has(failedModel)
|
|
|
|
|
) {
|
2025-11-18 12:01:16 -05:00
|
|
|
isModelNotFoundError = true;
|
|
|
|
|
const messageLines = [
|
2025-12-17 09:43:21 -08:00
|
|
|
`It seems like you don't have access to ${failedModel}.`,
|
2025-11-18 12:01:16 -05:00
|
|
|
`Learn more at https://goo.gle/enable-preview-features`,
|
2025-12-17 09:43:21 -08:00
|
|
|
`To disable ${failedModel}, disable "Preview features" in /settings.`,
|
2025-11-06 17:13:38 -05:00
|
|
|
];
|
|
|
|
|
message = messageLines.join('\n');
|
2025-09-08 16:19:52 -04:00
|
|
|
} else {
|
2025-12-17 09:43:21 -08:00
|
|
|
const messageLines = [
|
|
|
|
|
`We are currently experiencing high demand.`,
|
|
|
|
|
'We apologize and appreciate your patience.',
|
|
|
|
|
'/model to switch models.',
|
|
|
|
|
];
|
|
|
|
|
message = messageLines.join('\n');
|
2025-11-06 17:13:38 -05:00
|
|
|
}
|
|
|
|
|
|
2025-09-08 16:19:52 -04:00
|
|
|
setModelSwitchedFromQuotaError(true);
|
|
|
|
|
config.setQuotaErrorOccurred(true);
|
|
|
|
|
|
2025-11-06 13:43:21 -05:00
|
|
|
if (isDialogPending.current) {
|
|
|
|
|
return 'stop'; // A dialog is already active, so just stop this request.
|
2025-09-08 16:19:52 -04:00
|
|
|
}
|
2025-11-06 13:43:21 -05:00
|
|
|
isDialogPending.current = true;
|
|
|
|
|
|
|
|
|
|
const intent: FallbackIntent = await new Promise<FallbackIntent>(
|
|
|
|
|
(resolve) => {
|
|
|
|
|
setProQuotaRequest({
|
|
|
|
|
failedModel,
|
|
|
|
|
fallbackModel,
|
|
|
|
|
resolve,
|
2025-11-18 12:01:16 -05:00
|
|
|
message,
|
|
|
|
|
isTerminalQuotaError,
|
|
|
|
|
isModelNotFoundError,
|
2025-11-06 13:43:21 -05:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
2025-09-08 16:19:52 -04:00
|
|
|
|
2025-11-06 13:43:21 -05:00
|
|
|
return intent;
|
2025-09-08 16:19:52 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
config.setFallbackModelHandler(fallbackHandler);
|
|
|
|
|
}, [config, historyManager, userTier, setModelSwitchedFromQuotaError]);
|
|
|
|
|
|
|
|
|
|
const handleProQuotaChoice = useCallback(
|
2025-11-06 13:43:21 -05:00
|
|
|
(choice: FallbackIntent) => {
|
2025-09-08 16:19:52 -04:00
|
|
|
if (!proQuotaRequest) return;
|
|
|
|
|
|
2025-11-06 13:43:21 -05:00
|
|
|
const intent: FallbackIntent = choice;
|
2025-09-08 16:19:52 -04:00
|
|
|
proQuotaRequest.resolve(intent);
|
|
|
|
|
setProQuotaRequest(null);
|
|
|
|
|
isDialogPending.current = false; // Reset the flag here
|
|
|
|
|
|
2025-11-18 12:01:16 -05:00
|
|
|
if (choice === 'retry_always') {
|
2025-12-17 09:43:21 -08:00
|
|
|
// Explicitly set the model to the fallback model to persist the user's choice.
|
|
|
|
|
// This ensures the Footer updates and future turns use this model.
|
|
|
|
|
config.setModel(proQuotaRequest.fallbackModel);
|
|
|
|
|
|
|
|
|
|
historyManager.addItem(
|
|
|
|
|
{
|
|
|
|
|
type: MessageType.INFO,
|
|
|
|
|
text: `Switched to fallback model ${proQuotaRequest.fallbackModel}`,
|
|
|
|
|
},
|
|
|
|
|
Date.now(),
|
|
|
|
|
);
|
2025-09-08 16:19:52 -04:00
|
|
|
}
|
|
|
|
|
},
|
2025-12-17 09:43:21 -08:00
|
|
|
[proQuotaRequest, historyManager, config],
|
2025-09-08 16:19:52 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
proQuotaRequest,
|
|
|
|
|
handleProQuotaChoice,
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-11-18 12:01:16 -05:00
|
|
|
|
|
|
|
|
function getResetTimeMessage(delayMs: number): string {
|
|
|
|
|
const resetDate = new Date(Date.now() + delayMs);
|
|
|
|
|
|
|
|
|
|
const timeFormatter = new Intl.DateTimeFormat('en-US', {
|
|
|
|
|
hour: 'numeric',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
timeZoneName: 'short',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return `Access resets at ${timeFormatter.format(resetDate)}.`;
|
|
|
|
|
}
|