2026-06-14 19:31:09 +07:00
/**
* Google usage handlers (Gemini CLI + Antigravity)
*/
2026-07-08 10:09:03 +07:00
import { CLIENT_METADATA } from "../../config/appConstants.js" ;
import { ANTIGRAVITY_IDE_USER_AGENT , ANTIGRAVITY_IDE_VERSION , ANTIGRAVITY_OAUTH_CLIENT } from "../../providers/shared.js" ;
2026-06-14 19:31:09 +07:00
import { U , parseResetTime , normalizeCloudCodeProjectId , fetchWithTimeout } from "./shared.js" ;
2026-09-09 09:54:40 +07:00
import { fetchAntigravityWeeklyQuota } from "./antigravity-weekly.js" ;
2026-06-14 19:31:09 +07:00
// Antigravity API config (from Quotio) — urls from registry, oauth client + dynamic UA kept here
const ANTIGRAVITY_CONFIG = {
... U ( "antigravity" ),
... ANTIGRAVITY_OAUTH_CLIENT ,
2026-07-08 10:09:03 +07:00
userAgent : ANTIGRAVITY_IDE_USER_AGENT ,
2026-06-14 19:31:09 +07:00
};
/**
* Gemini CLI Usage — fetch per-model quota via Cloud Code Assist API.
* Uses retrieveUserQuota (same endpoint as `gemini /stats`) returning
* per-model buckets with remainingFraction + resetTime.
*/
export async function getGeminiUsage ( accessToken , providerSpecificData , proxyOptions = null ) {
if ( ! accessToken ) {
return { plan : "Free" , message : "Gemini CLI access token not available." };
}
try {
// Resolve project id: prefer connection-stored id, else loadCodeAssist lookup.
// #1271: OAuth save stores projectId on the connection, not providerSpecificData.
let projectId = normalizeCloudCodeProjectId ( providerSpecificData ? . projectId );
let plan = "Free" ;
if ( ! projectId ) {
const subInfo = await getGeminiSubscriptionInfo ( accessToken , proxyOptions );
projectId = normalizeCloudCodeProjectId ( subInfo ? . cloudaicompanionProject );
plan = subInfo ? . currentTier ? . name || plan ;
}
if ( ! projectId ) {
return {
plan ,
message : "Gemini CLI project ID not available. Reconnect Gemini CLI, or configure a Google Cloud project with Gemini Code Assist access before checking quota." ,
};
}
const response = await fetchWithTimeout (
U ( "gemini-cli" ). quotaUrl ,
{
method : "POST" ,
headers : {
Authorization : `Bearer ${ accessToken } ` ,
"Content-Type" : "application/json" ,
},
body : JSON . stringify ({ project : projectId }),
},
10000 ,
proxyOptions
);
if ( ! response . ok ) {
return { plan , message : `Gemini CLI quota error ( ${ response . status } ).` };
}
const data = await response . json ();
const quotas = {};
if ( Array . isArray ( data . buckets )) {
for ( const bucket of data . buckets ) {
if ( ! bucket . modelId || bucket . remainingFraction == null ) continue ;
const remainingFraction = Number ( bucket . remainingFraction ) || 0 ;
const total = 1000 ; // Normalized base, matches antigravity convention
const remaining = Math . round ( total * remainingFraction );
const used = Math . max ( 0 , total - remaining );
quotas [ bucket . modelId ] = {
used ,
total ,
resetAt : parseResetTime ( bucket . resetTime ),
remainingPercentage : remainingFraction * 100 ,
unlimited : false ,
};
}
}
return { plan , quotas };
} catch ( error ) {
return { message : `Gemini CLI error: ${ error . message } ` };
}
}
/**
* Get Gemini CLI subscription info via loadCodeAssist
*/
async function getGeminiSubscriptionInfo ( accessToken , proxyOptions = null ) {
try {
const response = await fetchWithTimeout (
U ( "gemini-cli" ). loadCodeAssistUrl ,
{
method : "POST" ,
headers : {
Authorization : `Bearer ${ accessToken } ` ,
"Content-Type" : "application/json" ,
},
body : JSON . stringify ({ metadata : CLIENT_METADATA }),
},
10000 ,
proxyOptions
);
if ( ! response . ok ) return null ;
return await response . json ();
} catch {
return null ;
}
}
/**
* Antigravity Usage - Fetch quota from Google Cloud Code API
*/
export async function getAntigravityUsage ( accessToken , providerSpecificData , proxyOptions = null ) {
try {
// Fetch subscription info once — reuse for both projectId and plan
const subscriptionInfo = await getAntigravitySubscriptionInfo ( accessToken , proxyOptions );
const projectId = subscriptionInfo ? . cloudaicompanionProject || null ;
const response = await fetchWithTimeout ( ANTIGRAVITY_CONFIG . quotaApiUrl , {
method : "POST" ,
headers : {
"Authorization" : `Bearer ${ accessToken } ` ,
"User-Agent" : ANTIGRAVITY_CONFIG . userAgent ,
"Content-Type" : "application/json" ,
"X-Client-Name" : "antigravity" ,
2026-07-08 10:09:03 +07:00
"X-Client-Version" : ANTIGRAVITY_IDE_VERSION ,
2026-06-14 19:31:09 +07:00
},
body : JSON . stringify ({
...( projectId ? { project : projectId } : {})
}),
}, 10000 , proxyOptions );
if ( response . status === 403 ) {
return {
message : "Antigravity quota API access forbidden. Chat may still work." ,
quotas : {}
};
}
if ( response . status === 401 ) {
return {
message : "Antigravity quota API authentication expired. Chat may still work." ,
quotas : {}
};
}
if ( ! response . ok ) {
throw new Error ( `Antigravity API error: ${ response . status } ` );
}
const data = await response . json ();
const quotas = {};
2026-09-09 09:54:40 +07:00
// Detect tier: free-tier accounts only have weekly quotas (no separate 5h window).
// On free-tier, fetchAvailableModels returns misleading per-model quota info
// (missing remainingFraction defaults to 0, or reflects the weekly limit not a 5h window).
const paidTierId = subscriptionInfo ? . paidTier ? . id ;
const isFreeTier = ! paidTierId || paidTierId === "free-tier" ;
// Parse model quotas only for paid-tier accounts.
// Free-tier accounts skip this — their only meaningful quota is the weekly limit.
if ( ! isFreeTier && data . models ) {
2026-06-14 19:31:09 +07:00
// Filter only recommended/important models (must match PROVIDER_MODELS ag ids)
const importantModels = [
2026-09-03 09:12:52 +07:00
'gemini-3.8-flash-high' ,
'gemini-3.8-flash-medium' ,
'gemini-3.8-flash-low' ,
2026-08-14 16:26:45 +07:00
'gemini-3.7-flash-high' ,
'gemini-3.7-flash-medium' ,
'gemini-3.7-flash-low' ,
2026-08-05 13:19:49 +07:00
'gemini-3.6-flash-high' ,
'gemini-3.6-flash-medium' ,
'gemini-3.6-flash-low' ,
2026-06-14 19:31:09 +07:00
'gemini-3.5-flash-low' ,
'gemini-3.5-flash-extra-low' ,
'gemini-pro-agent' ,
'gemini-3.1-pro-low' ,
'claude-sonnet-4-6' ,
'claude-opus-4-6-thinking' ,
'gpt-oss-120b-medium' ,
2026-06-21 17:50:21 +07:00
// Image generation models
'gemini-3.1-flash-image' ,
2026-06-14 19:31:09 +07:00
];
for ( const [ modelKey , info ] of Object . entries ( data . models )) {
// Skip models without quota info
if ( ! info . quotaInfo ) {
continue ;
}
// Skip internal models and non-important models
if ( info . isInternal || ! importantModels . includes ( modelKey )) {
continue ;
}
const remainingFraction = info . quotaInfo . remainingFraction || 0 ;
const remainingPercentage = remainingFraction * 100 ;
// Convert percentage to used/total for UI compatibility
const total = 1000 ; // Normalized base
const remaining = Math . round ( total * remainingFraction );
const used = total - remaining ;
// Use modelKey as key (matches PROVIDER_MODELS id)
quotas [ modelKey ] = {
used ,
total ,
resetAt : parseResetTime ( info . quotaInfo . resetTime ),
remainingPercentage ,
unlimited : false ,
displayName : info . displayName || modelKey ,
};
}
}
2026-09-09 09:54:40 +07:00
// Best-effort weekly quota overlay — never blocks or breaks per-model results
try {
const weeklyQuotas = await fetchAntigravityWeeklyQuota (
accessToken ,
projectId ,
proxyOptions
);
// Reconcile weekly quota against model family status:
// If every model in a family is locked/exhausted (remainingPercentage === 0)
// until a future reset time, the weekly limit cannot be 100% available.
// On Google's Free Starter tier, retrieveUserQuotaSummary buggily reports
// remainingFraction: 1 even after the starter quota is depleted and all models 429.
const entries = Object . entries ( quotas );
const geminiModels = entries . filter (([ k ]) => k . startsWith ( "gemini-" ) && ! k . includes ( "image" ));
const claudeModels = entries . filter (([ k ]) => k . startsWith ( "claude-" ));
if ( weeklyQuotas . gemini_weekly && geminiModels . length > 0 ) {
const allGeminiExhausted = geminiModels . every (([, q ]) => ( q . remainingPercentage ?? 0 ) === 0 );
if ( allGeminiExhausted && weeklyQuotas . gemini_weekly . remainingPercentage > 0 ) {
const maxResetAt = geminiModels . reduce (( max , [, q ]) =>
! max || ( q . resetAt && new Date ( q . resetAt ) > new Date ( max )) ? q . resetAt : max , null
);
weeklyQuotas . gemini_weekly . used = weeklyQuotas . gemini_weekly . total ;
weeklyQuotas . gemini_weekly . remainingPercentage = 0 ;
if ( maxResetAt ) {
weeklyQuotas . gemini_weekly . resetAt = maxResetAt ;
}
}
}
if ( weeklyQuotas . claude_gpt_weekly && claudeModels . length > 0 ) {
const allClaudeExhausted = claudeModels . every (([, q ]) => ( q . remainingPercentage ?? 0 ) === 0 );
if ( allClaudeExhausted && weeklyQuotas . claude_gpt_weekly . remainingPercentage > 0 ) {
const maxResetAt = claudeModels . reduce (( max , [, q ]) =>
! max || ( q . resetAt && new Date ( q . resetAt ) > new Date ( max )) ? q . resetAt : max , null
);
weeklyQuotas . claude_gpt_weekly . used = weeklyQuotas . claude_gpt_weekly . total ;
weeklyQuotas . claude_gpt_weekly . remainingPercentage = 0 ;
if ( maxResetAt ) {
weeklyQuotas . claude_gpt_weekly . resetAt = maxResetAt ;
}
}
}
Object . assign ( quotas , weeklyQuotas );
} catch {
// Silently ignore — weekly is best-effort
}
2026-06-14 19:31:09 +07:00
return {
plan : subscriptionInfo ? . currentTier ? . name || "Unknown" ,
quotas ,
subscriptionInfo ,
};
} catch ( error ) {
console . error ( "[Antigravity Usage] Error:" , error . message , error . cause );
return { message : `Antigravity error: ${ error . message } ` };
}
}
/**
* Get Antigravity subscription info
*/
async function getAntigravitySubscriptionInfo ( accessToken , proxyOptions = null ) {
try {
const response = await fetchWithTimeout ( ANTIGRAVITY_CONFIG . loadProjectApiUrl , {
method : "POST" ,
headers : {
"Authorization" : `Bearer ${ accessToken } ` ,
"User-Agent" : ANTIGRAVITY_CONFIG . userAgent ,
"Content-Type" : "application/json" ,
},
body : JSON . stringify ({ metadata : CLIENT_METADATA , mode : 1 }),
}, 10000 , proxyOptions );
if ( ! response . ok ) return null ;
return await response . json ();
} catch ( error ) {
console . error ( "[Antigravity Subscription] Error:" , error . message );
return null ;
}
}