/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type { AvailableCredits, CreditType, GeminiUserTier, } from '../code_assist/types.js'; import { PREVIEW_GEMINI_MODEL, PREVIEW_GEMINI_3_1_MODEL, PREVIEW_GEMINI_FLASH_MODEL, } from '../config/models.js'; /** * Strategy for handling quota exhaustion when AI credits are available. * - 'ask': Prompt the user each time * - 'always': Automatically use credits * - 'never': Never use credits, show standard fallback */ export type OverageStrategy = 'ask' | 'always' | 'never'; /** Credit type for Google One AI credits */ export const G1_CREDIT_TYPE: CreditType = 'GOOGLE_ONE_AI'; /** * The set of models that support AI credits overage billing. * Only these models are eligible for the credits-based retry flow. */ export const OVERAGE_ELIGIBLE_MODELS = new Set([ PREVIEW_GEMINI_MODEL, PREVIEW_GEMINI_3_1_MODEL, PREVIEW_GEMINI_FLASH_MODEL, ]); /** * Checks if a model is eligible for AI credits overage billing. * @param model The model name to check. * @returns true if the model supports credits overage, false otherwise. */ export function isOverageEligibleModel(model: string): boolean { return OVERAGE_ELIGIBLE_MODELS.has(model); } /** Base URL for Google One AI page */ const G1_AI_BASE_URL = 'https://one.google.com/ai'; /** AccountChooser URL for redirecting with email context */ const ACCOUNT_CHOOSER_URL = 'https://accounts.google.com/AccountChooser'; /** UTM parameters for CLI tracking */ const UTM_SOURCE = 'gemini_cli'; // TODO: change to 'desktop' when G1 service fix is rolled out const UTM_MEDIUM = 'web'; /** * Wraps a URL in the AccountChooser redirect to maintain user context. * @param email User's email address for account selection * @param continueUrl The destination URL after account selection * @returns The full AccountChooser redirect URL */ export function wrapInAccountChooser( email: string, continueUrl: string, ): string { const params = new URLSearchParams({ Email: email, continue: continueUrl, }); return `${ACCOUNT_CHOOSER_URL}?${params.toString()}`; } /** * UTM campaign identifiers per the design doc. */ export const G1_UTM_CAMPAIGNS = { /** From Interception Flow "Manage" link (user has credits) */ MANAGE_ACTIVITY: 'hydrogen_cli_settings_ai_credits_activity_page', /** From "Manage" to add more credits */ MANAGE_ADD_CREDITS: 'hydrogen_cli_settings_add_credits', /** From Empty Wallet Flow "Get AI Credits" link */ EMPTY_WALLET_ADD_CREDITS: 'hydrogen_cli_insufficient_credits_add_credits', } as const; /** * Builds a G1 AI URL with UTM tracking parameters. * @param path The path segment (e.g., 'activity' or 'credits') * @param email User's email for AccountChooser wrapper * @param campaign The UTM campaign identifier * @returns The complete URL wrapped in AccountChooser */ export function buildG1Url( path: 'activity' | 'credits', email: string, campaign: string, ): string { const baseUrl = `${G1_AI_BASE_URL}/${path}`; const params = new URLSearchParams({ utm_source: UTM_SOURCE, utm_medium: UTM_MEDIUM, utm_campaign: campaign, }); const urlWithUtm = `${baseUrl}?${params.toString()}`; return wrapInAccountChooser(email, urlWithUtm); } /** * Extracts the G1 AI credit balance from a tier's available credits. * @param tier The user tier to check * @returns The credit amount as a number, 0 if eligible but empty, or null if not eligible */ export function getG1CreditBalance( tier: GeminiUserTier | null | undefined, ): number | null { if (!tier?.availableCredits) { return null; } const g1Credits = tier.availableCredits.filter( (credit: AvailableCredits) => credit.creditType === G1_CREDIT_TYPE, ); if (g1Credits.length === 0) { return null; } // creditAmount is an int64 represented as string; sum all matching entries return g1Credits.reduce((sum, credit) => { const amount = parseInt(credit.creditAmount ?? '0', 10); return sum + (isNaN(amount) ? 0 : amount); }, 0); } export const MIN_CREDIT_BALANCE = 50; /** * Determines if credits should be automatically used based on the overage strategy. * @param strategy The configured overage strategy * @param creditBalance The available credit balance * @returns true if credits should be auto-used, false otherwise */ export function shouldAutoUseCredits( strategy: OverageStrategy, creditBalance: number | null, ): boolean { return ( strategy === 'always' && creditBalance != null && creditBalance >= MIN_CREDIT_BALANCE ); } /** * Determines if the overage menu should be shown based on the strategy. * @param strategy The configured overage strategy * @param creditBalance The available credit balance * @returns true if the menu should be shown */ export function shouldShowOverageMenu( strategy: OverageStrategy, creditBalance: number | null, ): boolean { return ( strategy === 'ask' && creditBalance != null && creditBalance >= MIN_CREDIT_BALANCE ); } /** * Determines if the empty wallet menu should be shown. * @param strategy The configured overage strategy * @param creditBalance The available credit balance * @returns true if the empty wallet menu should be shown */ export function shouldShowEmptyWalletMenu( strategy: OverageStrategy, creditBalance: number | null, ): boolean { return ( strategy !== 'never' && creditBalance != null && creditBalance < MIN_CREDIT_BALANCE ); }