File size: 5,524 Bytes
7a1ad33 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | /**
* @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
);
}
|