import { z } from 'zod'; import { ConfigManager } from '../ipc/config/manager'; import { AuthServer } from '../ipc/cloud/authServer'; import { EnvHttpProxyAgent, ProxyAgent } from 'undici'; import { logger } from '../utils/logger'; import { buildUserAgent, FALLBACK_VERSION, resolveLocalInstalledVersion, } from '@/server/modules/proxy/request-user-agent'; import { isEmpty, isNumber, isString, isUndefined } from 'lodash-es'; // --- Constants & Config --- const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com'; const CLIENT_SECRET = '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'; const OAUTH_CLIENTS_ENV = 'ANTIGRAVITY_OAUTH_CLIENTS'; const ACTIVE_OAUTH_CLIENT_ENV = 'ANTIGRAVITY_OAUTH_CLIENT_KEY'; const DEFAULT_OAUTH_CLIENT_KEY = 'antigravity_enterprise'; const URLS = { TOKEN: 'https://oauth2.googleapis.com/token', USER_INFO: 'https://www.googleapis.com/oauth2/v2/userinfo', AUTH: 'https://accounts.google.com/o/oauth2/v2/auth', LOAD_PROJECT: 'https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist', DAILY_LOAD_PROJECT: 'https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist', FETCH_CREDITS: 'https://cloudcode-pa.googleapis.com/v1internal:fetchCredits', }; const QUOTA_API_ENDPOINTS = [ 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels', 'https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels', 'https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels', ] as const; // Request timeout in milliseconds (30 seconds) const REQUEST_TIMEOUT_MS = 30000; interface OAuthClientConfig { key: string; label: string; client_id: string; client_secret: string; is_builtin: boolean; } interface OAuthClientRegistry { clients: OAuthClientConfig[]; activeKey: string; } let cachedOAuthClientRegistry: OAuthClientRegistry | null = null; function normalizeClientKey(key: string): string { return key.trim().toLowerCase(); } function getClientByKey( clients: OAuthClientConfig[], clientKey: string | undefined, ): OAuthClientConfig | null { if (!clientKey) { return null; } const normalizedKey = normalizeClientKey(clientKey); return clients.find((client) => client.key === normalizedKey) ?? null; } function buildOAuthClientRegistry(): OAuthClientRegistry { const clients: OAuthClientConfig[] = [ { key: normalizeClientKey(DEFAULT_OAUTH_CLIENT_KEY), label: 'Antigravity Enterprise', client_id: CLIENT_ID, client_secret: CLIENT_SECRET, is_builtin: true, }, ]; const rawExtraClients = process.env[OAUTH_CLIENTS_ENV]; if (isString(rawExtraClients) && !isEmpty(rawExtraClients.trim())) { for (const entry of rawExtraClients.split(';')) { const trimmed = entry.trim(); if (trimmed === '') { continue; } const parts = trimmed.split('|').map((part) => part.trim()); if (parts.length < 3) { logger.warn( `[GoogleAPIService] Ignored invalid OAuth client entry in ${OAUTH_CLIENTS_ENV}: ${trimmed}`, ); continue; } const key = normalizeClientKey(parts[0]); const clientId = parts[1]; const clientSecret = parts[2]; if (key === '' || clientId === '' || clientSecret === '') { logger.warn( `[GoogleAPIService] Ignored incomplete OAuth client entry in ${OAUTH_CLIENTS_ENV}: ${trimmed}`, ); continue; } const clientConfig: OAuthClientConfig = { key, label: parts[3] && parts[3] !== '' ? parts[3] : key, client_id: clientId, client_secret: clientSecret, is_builtin: false, }; const existingIndex = clients.findIndex((client) => client.key === key); if (existingIndex >= 0) { clients[existingIndex] = clientConfig; } else { clients.push(clientConfig); } } } let activeKey = normalizeClientKey( process.env[ACTIVE_OAUTH_CLIENT_ENV] || DEFAULT_OAUTH_CLIENT_KEY, ); if (!clients.some((client) => client.key === activeKey)) { activeKey = clients[0]?.key ?? normalizeClientKey(DEFAULT_OAUTH_CLIENT_KEY); } return { clients, activeKey, }; } function getOAuthClientRegistry(): OAuthClientRegistry { if (cachedOAuthClientRegistry) { return cachedOAuthClientRegistry; } cachedOAuthClientRegistry = buildOAuthClientRegistry(); return cachedOAuthClientRegistry; } function getCandidateClients(preferredClientKey?: string): OAuthClientConfig[] { const registry = getOAuthClientRegistry(); const candidates: OAuthClientConfig[] = []; const seen = new Set(); const pushCandidate = (candidate: OAuthClientConfig | null) => { if (!candidate || seen.has(candidate.key)) { return; } seen.add(candidate.key); candidates.push(candidate); }; const preferred = getClientByKey(registry.clients, preferredClientKey); if (preferredClientKey && !preferred) { logger.warn( `[GoogleAPIService] Preferred OAuth client '${preferredClientKey}' not found; fallback to active client list`, ); } pushCandidate(preferred); pushCandidate(getClientByKey(registry.clients, registry.activeKey)); for (const client of registry.clients) { pushCandidate(client); } return candidates; } function selectAuthClient(clientKey?: string): OAuthClientConfig { const registry = getOAuthClientRegistry(); if (registry.clients.length === 0) { throw new Error('No OAuth clients configured'); } if (isString(clientKey) && !isEmpty(clientKey.trim())) { const selected = getClientByKey(registry.clients, clientKey); if (!selected) { throw new Error(`Unknown OAuth client key: ${clientKey}`); } return selected; } return getClientByKey(registry.clients, registry.activeKey) ?? registry.clients[0]; } function isClientMismatchError(status: number, errorText: string): boolean { const text = errorText.toLowerCase(); return ( status === 400 || status === 401 || status === 403 || text.includes('unauthorized_client') || text.includes('invalid_client') ); } /** * Creates an AbortSignal that times out after the specified duration. */ function createTimeoutSignal(ms: number): AbortSignal { const controller = new AbortController(); setTimeout(() => controller.abort(), ms); return controller.signal; } // --- Types --- export interface TokenResponse { access_token: string; expires_in: number; token_type: string; refresh_token?: string; scope?: string; oauth_client_key?: string; } export interface OAuthClientDescriptor { key: string; label: string; client_id: string; is_active: boolean; is_builtin: boolean; } export interface UserInfo { id: string; email: string; verified_email: boolean; name: string; given_name?: string; family_name?: string; picture?: string; } export const UserInfoSchema = z.object({ id: z.string(), email: z.string(), verified_email: z.boolean().optional().default(false), name: z.string().optional(), given_name: z.string().optional(), family_name: z.string().optional(), picture: z.string().optional(), }); export interface QuotaData { models: Record; model_forwarding_rules?: Record; subscription_tier?: string; is_forbidden?: boolean; ai_credits?: { credits: number; expiryDate: string }; } export interface ModelQuotaInfo { percentage: number; resetTime: string; display_name?: string; supports_images?: boolean; supports_thinking?: boolean; thinking_budget?: number; recommended?: boolean; max_tokens?: number; max_output_tokens?: number; supported_mime_types?: Record; } // Internal types for API parsing interface ModelInfoRaw { quotaInfo?: { remainingFraction?: number; resetTime?: string; }; displayName?: string; supportsImages?: boolean; supportsThinking?: boolean; thinkingBudget?: number; recommended?: boolean; maxTokens?: number; maxOutputTokens?: number; supportedMimeTypes?: Record; } interface DeprecatedModelInfoRaw { newModelId?: string; } interface IneligibleTierRaw { reasonCode?: string; } interface TierRaw { is_default?: boolean; id?: string; quotaTier?: string; name?: string; slug?: string; availableCredits?: AvailableCreditRaw[]; } interface AvailableCreditRaw { creditType?: string; creditAmount?: string | number; minimumCreditAmountForUsage?: string | number; } interface LoadProjectResponse { cloudaicompanionProject?: string; currentTier?: TierRaw; paidTier?: TierRaw; allowedTiers?: TierRaw[]; ineligibleTiers?: IneligibleTierRaw[]; } interface FetchModelsResponse { models?: Record; deprecatedModelIds?: Record; } interface ProjectContext { projectId?: string; subscriptionTier?: string; } function sleep(ms: number): Promise { return new Promise((resolve) => { setTimeout(resolve, ms); }); } function buildInternalApiHeaders(accessToken: string): Record { const discoveryVersion = resolveLocalInstalledVersion() ?? FALLBACK_VERSION; return { Authorization: `Bearer ${accessToken}`, 'User-Agent': buildUserAgent(discoveryVersion), 'Content-Type': 'application/json', }; } function resolveSubscriptionTier(payload: LoadProjectResponse): string | undefined { const paidTier = payload.paidTier; if (isString(paidTier?.name) && !isEmpty(paidTier.name.trim())) { return paidTier.name; } if (isString(paidTier?.id) && !isEmpty(paidTier.id.trim())) { return paidTier.id; } const ineligible = Array.isArray(payload.ineligibleTiers) && payload.ineligibleTiers.length > 0; if (!ineligible) { const currentTier = payload.currentTier; if (isString(currentTier?.name) && !isEmpty(currentTier.name.trim())) { return currentTier.name; } if (isString(currentTier?.id) && !isEmpty(currentTier.id.trim())) { return currentTier.id; } } if (Array.isArray(payload.allowedTiers)) { const preferredAllowedTier = payload.allowedTiers.find((tier) => tier.is_default === true) ?? payload.allowedTiers[0]; if (isString(preferredAllowedTier?.name) && !isEmpty(preferredAllowedTier.name.trim())) { return ineligible ? `${preferredAllowedTier.name} (Restricted)` : preferredAllowedTier.name; } if (isString(preferredAllowedTier?.id) && !isEmpty(preferredAllowedTier.id.trim())) { return ineligible ? `${preferredAllowedTier.id} (Restricted)` : preferredAllowedTier.id; } } return undefined; } function isTrackedModel(modelName: string): boolean { return /^(gemini|claude|gpt|image|imagen)/i.test(modelName); } function toModelQuotaInfo(modelName: string, info: ModelInfoRaw): ModelQuotaInfo | null { if (!isTrackedModel(modelName) || !info.quotaInfo) { return null; } const fraction = info.quotaInfo.remainingFraction ?? 0; return { percentage: Math.floor(fraction * 100), resetTime: info.quotaInfo.resetTime || '', display_name: info.displayName, supports_images: info.supportsImages, supports_thinking: info.supportsThinking, thinking_budget: info.thinkingBudget, recommended: info.recommended, max_tokens: info.maxTokens, max_output_tokens: info.maxOutputTokens, supported_mime_types: info.supportedMimeTypes, }; } function toModelForwardingRules( deprecatedModelIds: FetchModelsResponse['deprecatedModelIds'], ): Record | undefined { if (!deprecatedModelIds || Object.keys(deprecatedModelIds).length === 0) { return undefined; } const forwardingRules: Record = {}; for (const [oldModelId, deprecatedInfo] of Object.entries(deprecatedModelIds)) { if (isString(deprecatedInfo.newModelId) && deprecatedInfo.newModelId !== '') { forwardingRules[oldModelId] = deprecatedInfo.newModelId; } } return Object.keys(forwardingRules).length > 0 ? forwardingRules : undefined; } function parseCreditAmount(value: string | number | undefined): number { if (isNumber(value)) { return value; } if (isString(value)) { const parsed = Number.parseInt(value, 10); if (Number.isFinite(parsed)) { return parsed; } } return 0; } function toAiCredits( payload: Partial<{ credits: unknown; remainingCredits: unknown; expiryDate: unknown; expirationDate: unknown; }>, ): { credits: number; expiryDate: string } | null { const creditsValue = isNumber(payload.credits) || isString(payload.credits) ? payload.credits : isNumber(payload.remainingCredits) || isString(payload.remainingCredits) ? payload.remainingCredits : undefined; if (isUndefined(creditsValue)) { return null; } const expiryDate = isString(payload.expiryDate) ? payload.expiryDate : isString(payload.expirationDate) ? payload.expirationDate : ''; return { credits: parseCreditAmount(creditsValue), expiryDate, }; } function extractAiCreditsFromProjectContext( payload: LoadProjectResponse, ): { credits: number; expiryDate: string } | null { const availableCredit = payload.paidTier?.availableCredits?.[0]; if (!availableCredit) { return null; } return { credits: parseCreditAmount(availableCredit.creditAmount), expiryDate: '', }; } // --- Service Implementation --- export class GoogleAPIService { static listOAuthClients(): OAuthClientDescriptor[] { const registry = getOAuthClientRegistry(); return registry.clients.map((client) => { return { key: client.key, label: client.label, client_id: client.client_id, is_active: client.key === registry.activeKey, is_builtin: client.is_builtin, }; }); } static getActiveOAuthClientKey(): string { const registry = getOAuthClientRegistry(); return registry.activeKey; } static setActiveOAuthClientKey(clientKey: string): void { const registry = getOAuthClientRegistry(); const normalized = normalizeClientKey(clientKey); const exists = registry.clients.some((client) => client.key === normalized); if (!exists) { const available = registry.clients.map((client) => client.key).join(', '); throw new Error(`Unknown OAuth client key '${clientKey}'. Available: ${available}`); } registry.activeKey = normalized; process.env[ACTIVE_OAUTH_CLIENT_ENV] = normalized; } static normalizeRefreshedOAuthClientKey( currentToken: { oauth_client_key?: string; project_id?: string }, refreshedClientKey?: string, ): string | undefined { const resolved = refreshedClientKey ?? currentToken.oauth_client_key; const projectMissing = !isString(currentToken.project_id) || isEmpty(currentToken.project_id.trim()); if ( !isString(currentToken.oauth_client_key) && projectMissing && resolved && normalizeClientKey(resolved) === DEFAULT_OAUTH_CLIENT_KEY ) { logger.warn( '[GoogleAPIService] Refreshed token via enterprise client for a legacy account without project_id; keep oauth_client_key unset to avoid accidental enterprise lock', ); return undefined; } return resolved ? normalizeClientKey(resolved) : undefined; } private static getFetchOptions(proxyUrl?: string) { const proxyTraceEnabled = process.env.DEBUG_PROXY_TRACE === '1'; if (proxyUrl && proxyUrl.length > 0) { if (proxyTraceEnabled) { logger.info('[GoogleAPIService] Proxy source: account proxy_url'); } return { dispatcher: new ProxyAgent(proxyUrl), }; } try { const config = ConfigManager.loadConfig(); if (config.proxy?.upstream_proxy?.enabled) { if (!config.proxy.upstream_proxy.url) { throw new Error('Upstream proxy is enabled but URL is not configured'); } if (proxyTraceEnabled) { logger.info('[GoogleAPIService] Proxy source: config.proxy.upstream_proxy.url'); } return { dispatcher: new ProxyAgent(config.proxy.upstream_proxy.url), }; } } catch (e) { logger.error('[GoogleAPIService] Proxy configuration error', e); throw e; } const httpProxy = process.env.http_proxy?.trim() || process.env.HTTP_PROXY?.trim(); const httpsProxy = process.env.https_proxy?.trim() || process.env.HTTPS_PROXY?.trim(); const noProxy = process.env.no_proxy?.trim() || process.env.NO_PROXY?.trim(); const electronProxyServer = process.env.ELECTRON_PROXY_SERVER?.trim(); if (httpProxy || httpsProxy) { if (proxyTraceEnabled) { logger.info( `[GoogleAPIService] Proxy source: HTTP(S)_PROXY env (http: ${httpProxy ?? 'none'}, https: ${httpsProxy ?? 'none'})`, ); } return { dispatcher: new EnvHttpProxyAgent({ httpProxy, httpsProxy, noProxy, }), }; } if (electronProxyServer) { if (proxyTraceEnabled) { logger.info( `[GoogleAPIService] Proxy source: ELECTRON_PROXY_SERVER env (${electronProxyServer})`, ); } return { dispatcher: new ProxyAgent(electronProxyServer), }; } if (proxyTraceEnabled) { logger.info('[GoogleAPIService] Proxy source: none'); } return {}; } /** * Generates the OAuth2 authorization URL. */ static getAuthUrl(oauthClientKey?: string): string { const scopes = [ 'https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', 'https://www.googleapis.com/auth/cclog', 'https://www.googleapis.com/auth/experimentsandconfigs', ].join(' '); const oauthClient = selectAuthClient(oauthClientKey); const redirectUri = AuthServer.getRedirectUri(); const params = new URLSearchParams({ client_id: oauthClient.client_id, redirect_uri: redirectUri, response_type: 'code', scope: scopes, access_type: 'offline', prompt: 'consent', include_granted_scopes: 'true', }); return `${URLS.AUTH}?${params.toString()}`; } /** * Exchanges an authorization code for tokens. */ static async exchangeCode( code: string, proxyUrl?: string, preferredClientKey?: string, ): Promise { const redirectUri = AuthServer.getRedirectUri(); const candidates = getCandidateClients(preferredClientKey); if (candidates.length === 0) { throw new Error('No OAuth clients configured'); } const attemptErrors: string[] = []; for (const client of candidates) { const params = new URLSearchParams({ client_id: client.client_id, client_secret: client.client_secret, code, redirect_uri: redirectUri, grant_type: 'authorization_code', }); const response = await fetch(URLS.TOKEN, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params, signal: createTimeoutSignal(REQUEST_TIMEOUT_MS), ...this.getFetchOptions(proxyUrl), }).catch((err: unknown) => { if (err instanceof Error && err.name === 'AbortError') { throw new Error( 'Token exchange timed out. Please check your network connection and try again.', ); } throw err; }); if (response.ok) { const tokenResponse = (await response.json()) as TokenResponse; tokenResponse.oauth_client_key = client.key; return tokenResponse; } const text = await response.text(); attemptErrors.push(`${client.key} => ${text}`); if (isClientMismatchError(response.status, text)) { logger.warn( `[GoogleAPIService] Token exchange failed for OAuth client '${client.key}', trying next client`, ); continue; } throw new Error(`Token exchange failed for client [${client.key}]: ${text}`); } throw new Error(`Token exchange failed for all OAuth clients: ${attemptErrors.join(' | ')}`); } /** * Refreshes an access token using a refresh token. */ static async refreshAccessToken( refreshToken: string, proxyUrl?: string, preferredClientKey?: string, ): Promise { const candidates = getCandidateClients(preferredClientKey); if (candidates.length === 0) { throw new Error('No OAuth clients configured'); } const attemptErrors: string[] = []; for (const client of candidates) { const params = new URLSearchParams({ client_id: client.client_id, client_secret: client.client_secret, refresh_token: refreshToken, grant_type: 'refresh_token', }); const response = await fetch(URLS.TOKEN, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params, signal: createTimeoutSignal(REQUEST_TIMEOUT_MS), ...this.getFetchOptions(proxyUrl), }).catch((err: unknown) => { if (err instanceof Error && err.name === 'AbortError') { throw new Error( 'Token refresh timed out. Please check your network connection and try again.', ); } throw err; }); if (response.ok) { const tokenResponse = (await response.json()) as TokenResponse; tokenResponse.oauth_client_key = client.key; return tokenResponse; } const text = await response.text(); attemptErrors.push(`${client.key} => ${text}`); if (isClientMismatchError(response.status, text)) { logger.warn( `[GoogleAPIService] Token refresh failed for OAuth client '${client.key}', trying next client`, ); continue; } throw new Error(`Token refresh failed for client [${client.key}]: ${text}`); } throw new Error(`Token refresh failed for all OAuth clients: ${attemptErrors.join(' | ')}`); } /** * Fetches user profile information. */ static async getUserInfo(accessToken: string, proxyUrl?: string): Promise { const response = await fetch(URLS.USER_INFO, { headers: { Authorization: `Bearer ${accessToken}` }, signal: createTimeoutSignal(REQUEST_TIMEOUT_MS), ...this.getFetchOptions(proxyUrl), }).catch((err: unknown) => { if (err instanceof Error) { if (err.name === 'AbortError') { throw new Error( 'User info request timed out. Please check your network connection and try again.', ); } } throw err; }); if (!response.ok) { const text = await response.text(); throw new Error(`Failed to fetch user info: ${text}`); } const data = await response.json(); try { const parsed = UserInfoSchema.parse(data); return { ...parsed, // Google may omit profile claims such as family_name for accounts with limited profile data. name: parsed.name ?? parsed.email, }; } catch (err) { logger.error('[GoogleAPIService] Malformed user info response:', err); throw new Error('Received malformed user info from Google APIs'); } } public static async fetchProjectContext( accessToken: string, proxyUrl?: string, ): Promise { const body = { metadata: { ideType: 'ANTIGRAVITY' }, }; let projectId: string | undefined; let subscriptionTier: string | undefined; let lastError: any; for (let i = 0; i < 2; i++) { try { const response = await fetch(URLS.LOAD_PROJECT, { method: 'POST', headers: buildInternalApiHeaders(accessToken), body: JSON.stringify(body), signal: createTimeoutSignal(REQUEST_TIMEOUT_MS), ...this.getFetchOptions(proxyUrl), }); if (response.ok) { const data = (await response.json()) as LoadProjectResponse; if (isString(data.cloudaicompanionProject)) { projectId = data.cloudaicompanionProject; } subscriptionTier = resolveSubscriptionTier(data); break; } else { lastError = new Error(`HTTP ${response.status}: ${await response.text()}`); } } catch (error) { lastError = error; logger.warn(`[GoogleAPIService] Failed to fetch project ID (Attempt ${i + 1}):`, error); await sleep(500); } } if (!projectId && !subscriptionTier) { throw lastError || new Error('Failed to fetch project context after multiple attempts.'); } return { projectId, subscriptionTier, }; } public static async fetchProjectId( accessToken: string, proxyUrl?: string, ): Promise { const context = await this.fetchProjectContext(accessToken, proxyUrl); return context.projectId ?? null; } static async fetchAICredits( accessToken: string, proxyUrl?: string, ): Promise<{ credits: number; expiryDate: string } | null> { try { const fetchOptions = this.getFetchOptions(proxyUrl); const response = await fetch(URLS.FETCH_CREDITS, { method: 'POST', headers: buildInternalApiHeaders(accessToken), body: JSON.stringify({}), signal: createTimeoutSignal(REQUEST_TIMEOUT_MS), ...fetchOptions, }); if (response.ok) { const data = (await response.json()) as Partial<{ credits: number | string; remainingCredits: number | string; expiryDate: string; expirationDate: string; }>; return toAiCredits(data); } if (response.status !== 404) { return null; } // `fetchCredits` has started returning 404 for some accounts. Reuse the // internal `loadCodeAssist` payload, which also exposes paid tier credits. logger.warn('[GoogleAPIService] fetchCredits returned 404, falling back to loadCodeAssist'); const discoveryVersion = resolveLocalInstalledVersion() ?? FALLBACK_VERSION; const fallbackResponse = await fetch(URLS.DAILY_LOAD_PROJECT, { method: 'POST', headers: buildInternalApiHeaders(accessToken), body: JSON.stringify({ metadata: { ide_type: 'ANTIGRAVITY', ide_version: discoveryVersion, ide_name: 'antigravity', }, }), signal: createTimeoutSignal(REQUEST_TIMEOUT_MS), ...fetchOptions, }); if (!fallbackResponse.ok) { return null; } const fallbackData = (await fallbackResponse.json()) as LoadProjectResponse; return extractAiCreditsFromProjectContext(fallbackData); } catch { return null; } } /** * Core logic: Fetches detailed model quota information. */ static async fetchQuota(accessToken: string, proxyUrl?: string): Promise { const { projectId, subscriptionTier } = await this.fetchProjectContext(accessToken, proxyUrl); const payload: Record = projectId ? { project: projectId } : {}; let lastError: Error | null = null; const fetchOptions = this.getFetchOptions(proxyUrl); for (let endpointIndex = 0; endpointIndex < QUOTA_API_ENDPOINTS.length; endpointIndex++) { const endpoint = QUOTA_API_ENDPOINTS[endpointIndex]; const hasNextEndpoint = endpointIndex + 1 < QUOTA_API_ENDPOINTS.length; try { const response = await fetch(endpoint, { method: 'POST', headers: buildInternalApiHeaders(accessToken), body: JSON.stringify(payload), signal: createTimeoutSignal(REQUEST_TIMEOUT_MS), ...fetchOptions, }); if (!response.ok) { const text = await response.text(); const status = response.status; if (status === 403) { throw new Error('FORBIDDEN'); } if (status === 401) { throw new Error('UNAUTHORIZED'); } const errorMsg = `HTTP ${status} - ${text}`; if (hasNextEndpoint && (status === 429 || status >= 500)) { logger.warn( `[GoogleAPIService] Quota API ${endpoint} returned ${status}, falling back to next endpoint`, ); lastError = new Error(errorMsg); await sleep(1000); continue; } throw new Error(errorMsg); } const data = (await response.json()) as FetchModelsResponse; const result: QuotaData = { models: {}, subscription_tier: subscriptionTier, is_forbidden: false, }; for (const [modelName, modelInfoRaw] of Object.entries(data.models || {})) { const modelQuota = toModelQuotaInfo(modelName, modelInfoRaw); if (modelQuota) { result.models[modelName] = modelQuota; } } const modelForwardingRules = toModelForwardingRules(data.deprecatedModelIds); if (modelForwardingRules) { result.model_forwarding_rules = modelForwardingRules; } if (endpointIndex > 0) { logger.info( `[GoogleAPIService] Quota API fallback succeeded at endpoint #${endpointIndex + 1}`, ); } return result; } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); lastError = error instanceof Error ? error : new Error(String(error)); const isPermanentHttp4xx = /^HTTP 4\d{2}\b/.test(errorMsg) && !errorMsg.startsWith('HTTP 429'); // Abort retries for auth errors if (errorMsg === 'FORBIDDEN' || errorMsg === 'UNAUTHORIZED') { throw error; } if (hasNextEndpoint && !isPermanentHttp4xx) { logger.warn( `[GoogleAPIService] Quota API request failed at ${endpoint}: ${errorMsg}. Falling back to next endpoint`, ); await sleep(1000); continue; } throw lastError; } } throw lastError || new Error('Quota check failed'); } }