feat(billing): implement G1 AI credits overage flow with billing telemetry (#18590)

This commit is contained in:
Gaurav
2026-02-27 10:15:06 -08:00
committed by GitHub
parent fdd844b405
commit b2d6844f9b
55 changed files with 3182 additions and 23 deletions

View File

@@ -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);
}