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-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 ` ,
` /auth to switch to API key. ` ,
] . filter ( Boolean ) ;
message = messageLines . join ( '\n' ) ;
} else if ( error instanceof ModelNotFoundError ) {
isModelNotFoundError = true ;
const messageLines = [
` It seems like you don't have access to Gemini 3. ` ,
` Learn more at https://goo.gle/enable-preview-features ` ,
` To disable Gemini 3, 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-11-18 12:01:16 -05:00
message = ` ${ failedModel } is currently experiencing high demand. We apologize and appreciate your patience. ` ;
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' ) {
// If we were recovering from a Preview Model failure, show a specific message.
if ( proQuotaRequest . failedModel === PREVIEW_GEMINI_MODEL ) {
2025-11-25 16:17:22 -05:00
const showPeriodicalCheckMessage =
! proQuotaRequest . isModelNotFoundError &&
proQuotaRequest . fallbackModel === DEFAULT_GEMINI_MODEL ;
2025-11-18 12:01:16 -05:00
historyManager . addItem (
{
type : MessageType . INFO ,
2025-11-25 16:17:22 -05:00
text : ` Switched to fallback model ${ proQuotaRequest . fallbackModel } . ${ showPeriodicalCheckMessage ? ` We will periodically check if ${ PREVIEW_GEMINI_MODEL } is available again. ` : '' } ` ,
2025-11-18 12:01:16 -05:00
} ,
Date . now ( ) ,
) ;
} else {
historyManager . addItem (
{
type : MessageType . INFO ,
text : 'Switched to fallback model.' ,
} ,
Date . now ( ) ,
) ;
}
2025-09-08 16:19:52 -04:00
}
} ,
2025-11-06 13:43:21 -05:00
[ proQuotaRequest , historyManager ] ,
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 ) } . ` ;
}