feat(cliproxy): add entitlement evidence for gemini and agy

This commit is contained in:
Tam Nhu Tran
2026-04-10 13:17:50 -04:00
parent 70051c2acb
commit bb331ff5d8
14 changed files with 371 additions and 34 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ const GEMINI_CLI_QUOTA_GROUPS: GeminiCliQuotaGroupDefinition[] = [
id: 'gemini-flash-lite-series',
label: 'Gemini Flash Lite Series',
preferredModelId: 'gemini-2.5-flash-lite',
modelIds: ['gemini-2.5-flash-lite'],
modelIds: ['gemini-2.5-flash-lite', 'gemini-3.1-flash-lite-preview'],
},
{
id: 'gemini-flash-series',
@@ -0,0 +1,69 @@
import type { AccountTier } from './accounts/types';
import type {
ProviderAccessState,
ProviderCapacityState,
ProviderEntitlementEvidence,
ProviderEntitlementSource,
} from './provider-entitlement-types';
const RAW_TIER_LABELS: Record<string, string> = {
'free-tier': 'Free',
'legacy-tier': 'Legacy',
'standard-tier': 'Standard',
'g1-pro-tier': 'Pro',
'g1-ultra-tier': 'Ultra',
};
export function normalizeProviderTierId(rawTierId: string | null | undefined): AccountTier {
if (!rawTierId) return 'unknown';
const normalized = rawTierId.trim().toLowerCase();
if (!normalized) return 'unknown';
if (normalized.includes('ultra')) return 'ultra';
if (normalized.includes('pro')) return 'pro';
if (normalized.includes('free') || normalized.includes('legacy')) return 'free';
return 'unknown';
}
export function getProviderTierLabel(rawTierId: string | null | undefined): string | null {
if (!rawTierId) return null;
const normalized = rawTierId.trim().toLowerCase();
return normalized ? (RAW_TIER_LABELS[normalized] ?? rawTierId.trim()) : null;
}
export function buildProviderEntitlementEvidence(input: {
normalizedTier: AccountTier;
rawTierId?: string | null;
rawTierLabel?: string | null;
source: ProviderEntitlementSource;
confidence: 'high' | 'medium' | 'low';
accessState: ProviderAccessState;
capacityState: ProviderCapacityState;
notes?: string | null;
lastVerifiedAt?: number;
}): ProviderEntitlementEvidence {
const rawTierId = input.rawTierId?.trim() || null;
return {
normalizedTier: input.normalizedTier,
rawTierId,
rawTierLabel: input.rawTierLabel ?? getProviderTierLabel(rawTierId),
source: input.source,
confidence: input.confidence,
accessState: input.accessState,
capacityState: input.capacityState,
notes: input.notes ?? null,
lastVerifiedAt: input.lastVerifiedAt ?? Date.now(),
};
}
export function isModelCapacityExhausted(
message: string | null | undefined,
detail: string | null | undefined,
errorCode: string | null | undefined
): boolean {
const haystack = `${message || ''} ${detail || ''} ${errorCode || ''}`.toLowerCase();
return (
haystack.includes('model_capacity_exhausted') ||
haystack.includes('no capacity available') ||
haystack.includes('capacity exhausted')
);
}
@@ -0,0 +1,33 @@
import type { AccountTier } from './accounts/types';
export type ProviderEntitlementSource =
| 'runtime_api'
| 'runtime_inference'
| 'registry_cache'
| 'official_docs';
export type ProviderAccessState =
| 'entitled'
| 'not_entitled'
| 'capacity_exhausted'
| 'temporarily_unavailable'
| 'unknown';
export type ProviderCapacityState =
| 'available'
| 'capacity_exhausted'
| 'rate_limited'
| 'temporarily_unavailable'
| 'unknown';
export interface ProviderEntitlementEvidence {
normalizedTier: AccountTier;
rawTierId: string | null;
rawTierLabel: string | null;
source: ProviderEntitlementSource;
confidence: 'high' | 'medium' | 'low';
accessState: ProviderAccessState;
capacityState: ProviderCapacityState;
lastVerifiedAt: number;
notes?: string | null;
}
+62 -12
View File
@@ -8,7 +8,7 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { getAuthDir } from './config-generator';
import { getProviderAccounts, getPausedDir } from './account-manager';
import { getProviderAccounts, getPausedDir, setAccountTier } from './account-manager';
import { getTokenExpiryTimestamp, sanitizeEmail, isTokenExpired } from './auth-utils';
import { refreshGeminiToken } from './auth/gemini-token-refresh';
import {
@@ -16,6 +16,13 @@ import {
type GeminiCliParsedBucket,
} from './gemini-cli-quota-normalizer';
import type { GeminiCliQuotaResult, GeminiCliBucket } from './quota-types';
import {
buildProviderEntitlementEvidence,
getProviderTierLabel,
isModelCapacityExhausted,
normalizeProviderTierId,
} from './provider-entitlement-evidence';
import type { ProviderEntitlementEvidence } from './provider-entitlement-types';
/** Google Cloud Code API endpoints */
const GEMINI_CLI_API_BASE = 'https://cloudcode-pa.googleapis.com';
@@ -25,13 +32,6 @@ const GEMINI_CLI_CODE_ASSIST_URL = `${GEMINI_CLI_API_BASE}/${GEMINI_CLI_API_VERS
const GEMINI_CLI_ERROR_DETAIL_MAX_LENGTH = 320;
const GEMINI_CLI_ERROR_DETAIL_TRUNCATION_SUFFIX = '...[truncated]';
const GEMINI_CLI_G1_CREDIT_TYPE = 'GOOGLE_ONE_AI';
const GEMINI_CLI_TIER_LABELS: Record<string, string> = {
'free-tier': 'Free',
'legacy-tier': 'Legacy',
'standard-tier': 'Standard',
'g1-pro-tier': 'Pro',
'g1-ultra-tier': 'Ultra',
};
/** Auth data extracted from Gemini CLI auth file */
interface GeminiCliAuthData {
@@ -90,6 +90,7 @@ interface GeminiCliSupplementaryInfo {
tierLabel: string | null;
tierId: string | null;
creditBalance: number | null;
normalizedTier: 'free' | 'pro' | 'ultra' | 'unknown';
}
/**
@@ -278,8 +279,7 @@ function resolveGeminiCliTierId(payload: GeminiCliCodeAssistResponse | null): st
function resolveGeminiCliTierLabel(payload: GeminiCliCodeAssistResponse | null): string | null {
const tierId = resolveGeminiCliTierId(payload);
if (!tierId) return null;
return GEMINI_CLI_TIER_LABELS[tierId] ?? tierId;
return getProviderTierLabel(tierId);
}
function resolveGeminiCliCreditBalance(payload: GeminiCliCodeAssistResponse | null): number | null {
@@ -340,7 +340,7 @@ async function fetchGeminiCliSupplementary(
if (verbose) {
console.error(`[i] Gemini CLI supplementary metadata unavailable: HTTP ${response.status}`);
}
return { tierLabel: null, tierId: null, creditBalance: null };
return { tierLabel: null, tierId: null, creditBalance: null, normalizedTier: 'unknown' };
}
const payload = (await response.json()) as GeminiCliCodeAssistResponse;
@@ -348,6 +348,7 @@ async function fetchGeminiCliSupplementary(
tierLabel: resolveGeminiCliTierLabel(payload),
tierId: resolveGeminiCliTierId(payload),
creditBalance: resolveGeminiCliCreditBalance(payload),
normalizedTier: normalizeProviderTierId(resolveGeminiCliTierId(payload)),
};
} catch (error) {
clearTimeout(timeoutId);
@@ -355,7 +356,7 @@ async function fetchGeminiCliSupplementary(
const message = error instanceof Error ? error.message : 'Unknown error';
console.error(`[i] Gemini CLI supplementary metadata skipped: ${message}`);
}
return { tierLabel: null, tierId: null, creditBalance: null };
return { tierLabel: null, tierId: null, creditBalance: null, normalizedTier: 'unknown' };
}
}
@@ -371,6 +372,7 @@ function buildGeminiCliFailureResult(
retryable?: boolean;
needsReauth?: boolean;
isForbidden?: boolean;
entitlement?: ProviderEntitlementEvidence;
}
): GeminiCliQuotaResult {
return {
@@ -390,6 +392,7 @@ function buildGeminiCliFailureResult(
retryable: options.retryable,
needsReauth: options.needsReauth,
isForbidden: options.isForbidden,
entitlement: options.entitlement,
};
}
@@ -549,10 +552,37 @@ function buildGeminiCliHttpFailureResult(
actionHint: buildGeminiCliForbiddenActionHint(parsed),
isForbidden: true,
retryable: false,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'medium',
accessState: 'not_entitled',
capacityState: 'unknown',
}),
});
}
if (status === 429) {
if (isModelCapacityExhausted(parsed.message, parsed.errorDetail, parsed.errorCode)) {
return buildGeminiCliFailureResult(accountId, projectId, {
error: parsed.message || 'Model capacity exhausted for this account right now',
httpStatus: 429,
errorCode: 'capacity_exhausted',
errorDetail: parsed.errorDetail,
actionHint:
'Retry later or switch to another Gemini model. This indicates temporary model capacity, not an authentication failure.',
retryable: true,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'medium',
accessState: 'entitled',
capacityState: 'capacity_exhausted',
notes: 'Upstream returned MODEL_CAPACITY_EXHAUSTED for this model.',
}),
});
}
return buildGeminiCliFailureResult(accountId, projectId, {
error: parsed.message || 'Rate limited - try again later',
httpStatus: 429,
@@ -560,6 +590,13 @@ function buildGeminiCliHttpFailureResult(
errorDetail: parsed.errorDetail,
actionHint: 'Retry after a short delay.',
retryable: true,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'low',
accessState: 'unknown',
capacityState: 'rate_limited',
}),
});
}
@@ -683,6 +720,10 @@ async function fetchWithAuthData(
if (verbose) console.error(`[i] Gemini CLI buckets found: ${buckets.length}`);
if (supplementary.normalizedTier !== 'unknown') {
setAccountTier('gemini', accountId, supplementary.normalizedTier);
}
return {
success: true,
buckets,
@@ -690,6 +731,15 @@ async function fetchWithAuthData(
tierLabel: supplementary.tierLabel,
tierId: supplementary.tierId,
creditBalance: supplementary.creditBalance,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: supplementary.normalizedTier,
rawTierId: supplementary.tierId,
rawTierLabel: supplementary.tierLabel,
source: supplementary.tierId ? 'runtime_api' : 'runtime_inference',
confidence: supplementary.tierId ? 'high' : 'medium',
accessState: 'entitled',
capacityState: 'available',
}),
lastUpdated: Date.now(),
accountId,
};
+31 -18
View File
@@ -17,6 +17,12 @@ import {
type AccountTier,
} from './account-manager';
import { sanitizeEmail, isTokenExpired } from './auth-utils';
import {
buildProviderEntitlementEvidence,
getProviderTierLabel,
normalizeProviderTierId,
} from './provider-entitlement-evidence';
import type { ProviderEntitlementEvidence } from './provider-entitlement-types';
import { buildManagementHeaders, buildProxyUrl, getProxyTarget } from './proxy-target-resolver';
/** Individual model quota info */
@@ -67,6 +73,8 @@ export interface QuotaResult {
projectId?: string;
/** Detected account tier based on model access */
tier?: AccountTier;
/** Richer provider entitlement evidence derived from live/runtime signals */
entitlement?: ProviderEntitlementEvidence;
}
/** Google Cloud Code API endpoints */
@@ -175,6 +183,8 @@ interface ManagedResponse {
interface ProjectLookupResult {
projectId: string | null;
tier?: AccountTier;
rawTierId?: string | null;
rawTierLabel?: string | null;
error?: string;
errorCode?: string;
errorDetail?: string;
@@ -518,21 +528,6 @@ function readAuthData(provider: CLIProxyProvider, accountId: string): AuthData |
* API returns: "g1-ultra-tier", "g1-pro-tier", "standard-tier", etc.
* Priority: ultra > pro > free
*/
function mapTierString(tierStr: string | undefined): AccountTier {
if (!tierStr) return 'unknown';
const normalized = tierStr.toLowerCase();
// Match "g1-ultra-tier" or "ultra" anywhere in string
if (normalized.includes('ultra')) return 'ultra';
// Match "g1-pro-tier" or "pro" anywhere in string
if (normalized.includes('pro')) return 'pro';
// Match free/legacy tiers
if (normalized.includes('free') || normalized.includes('legacy')) {
return 'free';
}
// "standard-tier" and other unknown values = unknown
return 'unknown';
}
/**
* Get project ID and tier via loadCodeAssist endpoint
* Uses paidTier.id for accurate tier detection (g1-ultra-tier, g1-pro-tier)
@@ -590,10 +585,15 @@ async function getProjectId(accountId: string, accessToken: string): Promise<Pro
}
// Extract tier - paidTier reflects actual subscription status, takes priority
const tierStr = data.paidTier?.id || data.currentTier?.id;
const tier = mapTierString(tierStr);
const rawTierId = (data.paidTier?.id || data.currentTier?.id || '').trim() || null;
const tier = normalizeProviderTierId(rawTierId);
return { projectId: projectId.trim(), tier };
return {
projectId: projectId.trim(),
tier,
rawTierId,
rawTierLabel: getProviderTierLabel(rawTierId),
};
}
/**
@@ -727,6 +727,8 @@ export async function fetchAccountQuota(
// Get project ID and tier - prefer stored project ID, but always call API for tier
let projectId = authData.projectId;
let apiTier: AccountTier = 'unknown';
let rawTierId: string | null = null;
let rawTierLabel: string | null = null;
// Always call loadCodeAssist to get accurate tier from API.
// If the file token is stale, the helper retries through CLIProxy management auth.
@@ -755,6 +757,8 @@ export async function fetchAccountQuota(
// Use API project ID if available, else fallback to stored
projectId = lastProjectResult.projectId || projectId;
apiTier = lastProjectResult.tier || 'unknown';
rawTierId = lastProjectResult.rawTierId || null;
rawTierLabel = lastProjectResult.rawTierLabel || null;
if (verbose) console.error(`[i] Project ID: ${projectId || 'not found'}`);
@@ -769,6 +773,15 @@ export async function fetchAccountQuota(
if (result.success) {
const finalTier = apiTier !== 'unknown' ? apiTier : 'unknown';
result.tier = finalTier;
result.entitlement = buildProviderEntitlementEvidence({
normalizedTier: finalTier,
rawTierId,
rawTierLabel,
source: rawTierId ? 'runtime_api' : 'runtime_inference',
confidence: rawTierId ? 'high' : 'medium',
accessState: 'entitled',
capacityState: 'available',
});
if (finalTier !== 'unknown') {
setAccountTier(provider, accountId, finalTier);
}
+4
View File
@@ -5,6 +5,8 @@
* Supports Antigravity, Codex, Claude, Gemini CLI, and GitHub Copilot OAuth providers.
*/
import type { ProviderEntitlementEvidence } from './provider-entitlement-types';
/** Supported quota providers */
export type QuotaProvider = 'agy' | 'codex' | 'claude' | 'gemini' | 'ghcp';
@@ -200,6 +202,8 @@ export interface GeminiCliQuotaResult extends QuotaErrorMetadata {
tierId?: string | null;
/** Available Google One AI credits when reported by the API */
creditBalance?: number | null;
/** Richer provider entitlement evidence derived from live/runtime signals */
entitlement?: ProviderEntitlementEvidence;
/** Timestamp of fetch */
lastUpdated: number;
/** Error message if fetch failed */
+12 -1
View File
@@ -103,6 +103,13 @@ function formatCliAccountLabel(account: { id: string; email?: string; nickname?:
return account.nickname ? `${account.nickname} (${displayName})` : displayName;
}
function resolveDisplayedTier(
accountTier: string | undefined,
liveTier: string | undefined
): string {
return (liveTier && liveTier !== 'unknown' ? liveTier : accountTier) || 'unknown';
}
interface QuotaFailureDisplayEntry {
tone: 'error' | 'info' | 'dim';
text: string;
@@ -363,7 +370,7 @@ function displayAntigravityQuotaSection(
if (isOnCooldown(provider, account.id)) statusParts.push(color('COOLDOWN', 'warning'));
const defaultMark = account.isDefault ? color('*', 'success') : ' ';
const tier = account.tier || 'unknown';
const tier = resolveDisplayedTier(account.tier, quota?.entitlement?.normalizedTier);
const status = statusParts.join(', ');
rows.push([defaultMark, formatCliAccountLabel(account), tier, avgQuota, status]);
@@ -641,6 +648,9 @@ function displayGeminiCliQuotaSection(
if (quota.tierLabel) {
console.log(` Tier: ${dim(quota.tierLabel)}`);
}
if (quota.entitlement?.rawTierId) {
console.log(` Tier ID: ${dim(quota.entitlement.rawTierId)}`);
}
if (quota.creditBalance !== null && quota.creditBalance !== undefined) {
console.log(` Credits: ${dim(quota.creditBalance.toLocaleString())}`);
}
@@ -783,6 +793,7 @@ const QUOTA_PROVIDER_RUNTIME: Record<QuotaSupportedProvider, QuotaProviderRuntim
export const __testExports = {
getQuotaFailureDisplayEntries,
resolveDisplayedTier,
};
export async function handleQuotaStatus(