mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-19 01:30:42 -07:00
feat(billing): implement G1 AI credits overage flow with billing telemetry (#18590)
This commit is contained in:
@@ -16,7 +16,9 @@ import {
|
||||
type UserTierId,
|
||||
VALID_GEMINI_MODELS,
|
||||
isProModel,
|
||||
isOverageEligibleModel,
|
||||
getDisplayString,
|
||||
type GeminiUserTier,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -24,12 +26,20 @@ import { MessageType } from '../types.js';
|
||||
import {
|
||||
type ProQuotaDialogRequest,
|
||||
type ValidationDialogRequest,
|
||||
type OverageMenuDialogRequest,
|
||||
type OverageMenuIntent,
|
||||
type EmptyWalletDialogRequest,
|
||||
type EmptyWalletIntent,
|
||||
} from '../contexts/UIStateContext.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { handleCreditsFlow } from './creditsFlowHandler.js';
|
||||
|
||||
interface UseQuotaAndFallbackArgs {
|
||||
config: Config;
|
||||
historyManager: UseHistoryManagerReturn;
|
||||
userTier: UserTierId | undefined;
|
||||
paidTier: GeminiUserTier | null | undefined;
|
||||
settings: LoadedSettings;
|
||||
setModelSwitchedFromQuotaError: (value: boolean) => void;
|
||||
onShowAuthSelection: () => void;
|
||||
}
|
||||
@@ -38,6 +48,8 @@ export function useQuotaAndFallback({
|
||||
config,
|
||||
historyManager,
|
||||
userTier,
|
||||
paidTier,
|
||||
settings,
|
||||
setModelSwitchedFromQuotaError,
|
||||
onShowAuthSelection,
|
||||
}: UseQuotaAndFallbackArgs) {
|
||||
@@ -45,9 +57,22 @@ export function useQuotaAndFallback({
|
||||
useState<ProQuotaDialogRequest | null>(null);
|
||||
const [validationRequest, setValidationRequest] =
|
||||
useState<ValidationDialogRequest | null>(null);
|
||||
// G1 AI Credits dialog states
|
||||
const [overageMenuRequest, setOverageMenuRequest] =
|
||||
useState<OverageMenuDialogRequest | null>(null);
|
||||
const [emptyWalletRequest, setEmptyWalletRequest] =
|
||||
useState<EmptyWalletDialogRequest | null>(null);
|
||||
const isDialogPending = useRef(false);
|
||||
const isValidationPending = useRef(false);
|
||||
|
||||
// Initial overage strategy from settings; runtime value read from config at call time.
|
||||
const initialOverageStrategy =
|
||||
(settings.merged.billing?.overageStrategy as
|
||||
| 'ask'
|
||||
| 'always'
|
||||
| 'never'
|
||||
| undefined) ?? 'ask';
|
||||
|
||||
// Set up Flash fallback handler
|
||||
useEffect(() => {
|
||||
const fallbackHandler: FallbackModelHandler = async (
|
||||
@@ -63,12 +88,52 @@ export function useQuotaAndFallback({
|
||||
const usageLimitReachedModel = isProModel(failedModel)
|
||||
? 'all Pro models'
|
||||
: failedModel;
|
||||
|
||||
if (error instanceof TerminalQuotaError) {
|
||||
isTerminalQuotaError = true;
|
||||
// Common part of the message for both tiers
|
||||
|
||||
const isInsufficientCredits = error.isInsufficientCredits;
|
||||
|
||||
// G1 Credits Flow: Only apply if user has a tier that supports credits
|
||||
// (paidTier?.availableCredits indicates the user is a G1 subscriber)
|
||||
// Skip if the error explicitly says they have insufficient credits (e.g. they
|
||||
// just exhausted them or zero balance cache is delayed).
|
||||
if (
|
||||
!isInsufficientCredits &&
|
||||
paidTier?.availableCredits &&
|
||||
isOverageEligibleModel(failedModel)
|
||||
) {
|
||||
const resetTime = error.retryDelayMs
|
||||
? getResetTimeMessage(error.retryDelayMs)
|
||||
: undefined;
|
||||
|
||||
const overageStrategy =
|
||||
config.getBillingSettings().overageStrategy ??
|
||||
initialOverageStrategy;
|
||||
|
||||
const creditsResult = await handleCreditsFlow({
|
||||
config,
|
||||
paidTier,
|
||||
overageStrategy,
|
||||
failedModel,
|
||||
fallbackModel,
|
||||
usageLimitReachedModel,
|
||||
resetTime,
|
||||
historyManager,
|
||||
setModelSwitchedFromQuotaError,
|
||||
isDialogPending,
|
||||
setOverageMenuRequest,
|
||||
setEmptyWalletRequest,
|
||||
});
|
||||
if (creditsResult) return creditsResult;
|
||||
}
|
||||
|
||||
// Default: Show existing ProQuotaDialog (for overageStrategy: 'never' or non-G1 users)
|
||||
const messageLines = [
|
||||
`Usage limit reached for ${usageLimitReachedModel}.`,
|
||||
error.retryDelayMs ? getResetTimeMessage(error.retryDelayMs) : null,
|
||||
error.retryDelayMs
|
||||
? `Access resets at ${getResetTimeMessage(error.retryDelayMs)}.`
|
||||
: null,
|
||||
`/stats model for usage details`,
|
||||
`/model to switch models.`,
|
||||
contentGeneratorConfig?.authType === AuthType.LOGIN_WITH_GOOGLE
|
||||
@@ -126,7 +191,16 @@ export function useQuotaAndFallback({
|
||||
};
|
||||
|
||||
config.setFallbackModelHandler(fallbackHandler);
|
||||
}, [config, historyManager, userTier, setModelSwitchedFromQuotaError]);
|
||||
}, [
|
||||
config,
|
||||
historyManager,
|
||||
userTier,
|
||||
paidTier,
|
||||
settings,
|
||||
initialOverageStrategy,
|
||||
setModelSwitchedFromQuotaError,
|
||||
onShowAuthSelection,
|
||||
]);
|
||||
|
||||
// Set up validation handler for 403 VALIDATION_REQUIRED errors
|
||||
useEffect(() => {
|
||||
@@ -204,11 +278,38 @@ export function useQuotaAndFallback({
|
||||
[validationRequest, onShowAuthSelection],
|
||||
);
|
||||
|
||||
// Handler for overage menu dialog (G1 AI Credits flow)
|
||||
const handleOverageMenuChoice = useCallback(
|
||||
(choice: OverageMenuIntent) => {
|
||||
if (!overageMenuRequest) return;
|
||||
|
||||
overageMenuRequest.resolve(choice);
|
||||
// State will be cleared by the effect callback after the promise resolves
|
||||
},
|
||||
[overageMenuRequest],
|
||||
);
|
||||
|
||||
// Handler for empty wallet dialog (G1 AI Credits flow)
|
||||
const handleEmptyWalletChoice = useCallback(
|
||||
(choice: EmptyWalletIntent) => {
|
||||
if (!emptyWalletRequest) return;
|
||||
|
||||
emptyWalletRequest.resolve(choice);
|
||||
// State will be cleared by the effect callback after the promise resolves
|
||||
},
|
||||
[emptyWalletRequest],
|
||||
);
|
||||
|
||||
return {
|
||||
proQuotaRequest,
|
||||
handleProQuotaChoice,
|
||||
validationRequest,
|
||||
handleValidationChoice,
|
||||
// G1 AI Credits
|
||||
overageMenuRequest,
|
||||
handleOverageMenuChoice,
|
||||
emptyWalletRequest,
|
||||
handleEmptyWalletChoice,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,5 +322,5 @@ function getResetTimeMessage(delayMs: number): string {
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
|
||||
return `Access resets at ${timeFormatter.format(resetDate)}.`;
|
||||
return timeFormatter.format(resetDate);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user