2026-01-05 10:37:09 +07:00
/**
* Usage Fetcher - Get usage data from provider APIs
*/
2026-03-12 16:20:46 +07:00
import { CLIENT_METADATA , getPlatformUserAgent } from "../config/appConstants.js" ;
2026-04-28 17:28:57 +07:00
import { proxyAwareFetch } from "../utils/proxyFetch.js" ;
2026-02-20 14:44:29 +07:00
2026-01-05 10:37:09 +07:00
// GitHub API config
const GITHUB_CONFIG = {
apiVersion : "2022-11-28" ,
userAgent : "GitHubCopilotChat/0.26.7" ,
};
2026-05-05 10:32:59 +07:00
// GLM quota endpoints (region-aware)
const GLM_QUOTA_URLS = {
international : "https://api.z.ai/api/monitor/usage/quota/limit" ,
china : "https://open.bigmodel.cn/api/monitor/usage/quota/limit" ,
};
// MiniMax usage endpoints (try in order, fallback on transient errors)
const MINIMAX_USAGE_URLS = {
minimax : [
"https://www.minimax.io/v1/token_plan/remains" ,
"https://api.minimax.io/v1/api/openplatform/coding_plan/remains" ,
],
"minimax-cn" : [
"https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains" ,
"https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains" ,
],
};
2026-01-05 10:37:09 +07:00
// Antigravity API config (from Quotio)
const ANTIGRAVITY_CONFIG = {
quotaApiUrl : "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels" ,
loadProjectApiUrl : "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist" ,
tokenUrl : "https://oauth2.googleapis.com/token" ,
clientId : "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" ,
clientSecret : "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" ,
2026-02-20 14:44:29 +07:00
userAgent : getPlatformUserAgent (),
2026-01-05 10:37:09 +07:00
};
// Codex (OpenAI) API config
const CODEX_CONFIG = {
usageUrl : "https://chatgpt.com/backend-api/wham/usage" ,
};
// Claude API config
const CLAUDE_CONFIG = {
2026-03-12 09:42:17 +07:00
oauthUsageUrl : "https://api.anthropic.com/api/oauth/usage" ,
2026-01-05 10:37:09 +07:00
usageUrl : "https://api.anthropic.com/v1/organizations/{org_id}/usage" ,
settingsUrl : "https://api.anthropic.com/v1/settings" ,
2026-03-12 09:42:17 +07:00
apiVersion : "2023-06-01" ,
2026-01-05 10:37:09 +07:00
};
/**
* Get usage data for a provider connection
* @param {Object} connection - Provider connection with accessToken
* @returns {Object} Usage data with quotas
*/
2026-04-28 17:28:57 +07:00
export async function getUsageForProvider ( connection , proxyOptions = null ) {
2026-05-26 06:23:47 +02:00
const { provider , accessToken , apiKey , providerSpecificData , projectId } = connection ;
const providerDataWithProjectId = {
...( providerSpecificData || {}),
...( projectId ? { projectId } : {}),
};
2026-01-05 10:37:09 +07:00
switch ( provider ) {
case "github" :
2026-04-28 17:28:57 +07:00
return await getGitHubUsage ( accessToken , providerSpecificData , proxyOptions );
2026-01-05 10:37:09 +07:00
case "gemini-cli" :
2026-05-26 06:23:47 +02:00
return await getGeminiUsage ( accessToken , providerDataWithProjectId , proxyOptions );
2026-01-05 10:37:09 +07:00
case "antigravity" :
2026-04-28 17:28:57 +07:00
return await getAntigravityUsage ( accessToken , providerSpecificData , proxyOptions );
2026-01-05 10:37:09 +07:00
case "claude" :
2026-04-28 17:28:57 +07:00
return await getClaudeUsage ( accessToken , proxyOptions );
2026-01-05 10:37:09 +07:00
case "codex" :
2026-04-28 17:28:57 +07:00
return await getCodexUsage ( accessToken , proxyOptions );
2026-02-04 09:54:11 +07:00
case "kiro" :
2026-04-28 17:28:57 +07:00
return await getKiroUsage ( accessToken , providerSpecificData , proxyOptions );
2026-05-23 16:31:50 +09:00
case "qoder" :
return await getQoderUsage ( accessToken , proxyOptions );
2026-01-05 10:37:09 +07:00
case "qwen" :
return await getQwenUsage ( accessToken , providerSpecificData );
case "iflow" :
return await getIflowUsage ( accessToken );
2026-04-21 23:32:28 -04:00
case "ollama" :
return await getOllamaUsage ( accessToken );
2026-05-05 10:32:59 +07:00
case "glm" :
case "glm-cn" :
return await getGlmUsage ( apiKey , provider , proxyOptions );
case "minimax" :
case "minimax-cn" :
return await getMiniMaxUsage ( apiKey , provider , proxyOptions );
2026-01-05 10:37:09 +07:00
default :
return { message : `Usage API not implemented for ${ provider } ` };
}
}
2026-02-06 11:23:08 +07:00
/**
* Parse reset date/time to ISO string
* Handles multiple formats: Unix timestamp (ms), ISO date string, etc.
*/
function parseResetTime ( resetValue ) {
if ( ! resetValue ) return null ;
2026-02-20 14:44:29 +07:00
2026-02-06 11:23:08 +07:00
try {
// If it's already a Date object
if ( resetValue instanceof Date ) {
return resetValue . toISOString ();
}
2026-02-20 14:44:29 +07:00
2026-04-28 10:05:54 +07:00
// Unix timestamps from provider APIs may be seconds or milliseconds.
2026-02-06 11:23:08 +07:00
if ( typeof resetValue === 'number' ) {
2026-04-28 10:05:54 +07:00
return new Date ( resetValue < 1e12 ? resetValue * 1000 : resetValue ). toISOString ();
2026-02-06 11:23:08 +07:00
}
2026-02-20 14:44:29 +07:00
2026-04-28 10:05:54 +07:00
// If it's a numeric string, treat it like a Unix timestamp too.
2026-02-06 11:23:08 +07:00
if ( typeof resetValue === 'string' ) {
2026-04-28 10:05:54 +07:00
if ( /^\d+$/ . test ( resetValue )) {
const timestamp = Number ( resetValue );
return new Date ( timestamp < 1e12 ? timestamp * 1000 : timestamp ). toISOString ();
}
2026-02-06 11:23:08 +07:00
return new Date ( resetValue ). toISOString ();
}
2026-02-20 14:44:29 +07:00
2026-02-06 11:23:08 +07:00
return null ;
} catch ( error ) {
console . warn ( `Failed to parse reset time: ${ resetValue } ` , error );
return null ;
}
}
2026-01-05 10:37:09 +07:00
/**
* GitHub Copilot Usage
2026-02-05 18:38:50 +07:00
* Uses GitHub accessToken (not copilotToken) to call copilot_internal/user API
2026-01-05 10:37:09 +07:00
*/
2026-04-28 17:28:57 +07:00
async function getGitHubUsage ( accessToken , providerSpecificData , proxyOptions = null ) {
2026-01-05 10:37:09 +07:00
try {
2026-02-05 18:38:50 +07:00
if ( ! accessToken ) {
throw new Error ( "No GitHub access token available. Please re-authorize the connection." );
}
2026-02-20 14:44:29 +07:00
2026-02-05 18:38:50 +07:00
// copilot_internal/user API requires GitHub OAuth token, not copilotToken
2026-04-28 17:28:57 +07:00
const response = await proxyAwareFetch ( "https://api.github.com/copilot_internal/user" , {
2026-01-05 10:37:09 +07:00
headers : {
2026-02-05 18:38:50 +07:00
"Authorization" : `token ${ accessToken } ` ,
2026-01-05 10:37:09 +07:00
"Accept" : "application/json" ,
"X-GitHub-Api-Version" : GITHUB_CONFIG . apiVersion ,
"User-Agent" : GITHUB_CONFIG . userAgent ,
2026-02-05 18:38:50 +07:00
"Editor-Version" : "vscode/1.100.0" ,
"Editor-Plugin-Version" : "copilot-chat/0.26.7" ,
2026-01-05 10:37:09 +07:00
},
2026-04-28 17:28:57 +07:00
}, proxyOptions );
2026-01-05 10:37:09 +07:00
if ( ! response . ok ) {
const error = await response . text ();
throw new Error ( `GitHub API error: ${ error } ` );
}
const data = await response . json ();
// Handle different response formats (paid vs free)
if ( data . quota_snapshots ) {
// Paid plan format
const snapshots = data . quota_snapshots ;
2026-02-06 11:23:08 +07:00
const resetAt = parseResetTime ( data . quota_reset_date );
2026-02-20 14:44:29 +07:00
2026-01-05 10:37:09 +07:00
return {
plan : data . copilot_plan ,
resetDate : data . quota_reset_date ,
quotas : {
2026-02-06 11:23:08 +07:00
chat : { ... formatGitHubQuotaSnapshot ( snapshots . chat ), resetAt },
completions : { ... formatGitHubQuotaSnapshot ( snapshots . completions ), resetAt },
premium_interactions : { ... formatGitHubQuotaSnapshot ( snapshots . premium_interactions ), resetAt },
2026-01-05 10:37:09 +07:00
},
};
} else if ( data . monthly_quotas || data . limited_user_quotas ) {
// Free/limited plan format
const monthlyQuotas = data . monthly_quotas || {};
const usedQuotas = data . limited_user_quotas || {};
2026-02-06 11:23:08 +07:00
const resetAt = parseResetTime ( data . limited_user_reset_date );
2026-02-20 14:44:29 +07:00
2026-01-05 10:37:09 +07:00
return {
plan : data . copilot_plan || data . access_type_sku ,
resetDate : data . limited_user_reset_date ,
quotas : {
chat : {
used : usedQuotas . chat || 0 ,
total : monthlyQuotas . chat || 0 ,
unlimited : false ,
2026-02-06 11:23:08 +07:00
resetAt ,
2026-01-05 10:37:09 +07:00
},
completions : {
used : usedQuotas . completions || 0 ,
total : monthlyQuotas . completions || 0 ,
unlimited : false ,
2026-02-06 11:23:08 +07:00
resetAt ,
2026-01-05 10:37:09 +07:00
},
},
};
}
return { message : "GitHub Copilot connected. Unable to parse quota data." };
} catch ( error ) {
throw new Error ( `Failed to fetch GitHub usage: ${ error . message } ` );
}
}
function formatGitHubQuotaSnapshot ( quota ) {
if ( ! quota ) return { used : 0 , total : 0 , unlimited : true };
2026-02-20 14:44:29 +07:00
2026-01-05 10:37:09 +07:00
return {
used : quota . entitlement - quota . remaining ,
total : quota . entitlement ,
remaining : quota . remaining ,
unlimited : quota . unlimited || false ,
};
}
/**
2026-05-05 10:32:59 +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.
2026-01-05 10:37:09 +07:00
*/
2026-05-05 10:32:59 +07:00
async function getGeminiUsage ( accessToken , providerSpecificData , proxyOptions = null ) {
if ( ! accessToken ) {
return { plan : "Free" , message : "Gemini CLI access token not available." };
}
try {
2026-05-26 06:23:47 +02:00
// 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 );
2026-05-05 10:32:59 +07:00
let plan = "Free" ;
if ( ! projectId ) {
const subInfo = await getGeminiSubscriptionInfo ( accessToken , proxyOptions );
2026-05-26 06:23:47 +02:00
projectId = normalizeCloudCodeProjectId ( subInfo ? . cloudaicompanionProject );
2026-05-05 10:32:59 +07:00
plan = subInfo ? . currentTier ? . name || plan ;
}
if ( ! projectId ) {
2026-05-26 06:23:47 +02:00
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." ,
};
2026-05-05 10:32:59 +07:00
}
const controller = new AbortController ();
const timeoutId = setTimeout (() => controller . abort (), 10000 );
let response ;
try {
response = await proxyAwareFetch (
"https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota" ,
{
method : "POST" ,
headers : {
Authorization : `Bearer ${ accessToken } ` ,
"Content-Type" : "application/json" ,
},
body : JSON . stringify ({ project : projectId }),
signal : controller . signal ,
},
proxyOptions
);
} finally {
clearTimeout ( timeoutId );
}
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 } ` };
}
}
2026-05-26 06:23:47 +02:00
function normalizeCloudCodeProjectId ( project ) {
if ( typeof project === "string" ) return project . trim () || null ;
if ( project && typeof project === "object" && typeof project . id === "string" ) {
return project . id . trim () || null ;
}
return null ;
}
2026-05-05 10:32:59 +07:00
/**
* Get Gemini CLI subscription info via loadCodeAssist
*/
async function getGeminiSubscriptionInfo ( accessToken , proxyOptions = null ) {
const controller = new AbortController ();
const timeoutId = setTimeout (() => controller . abort (), 10000 );
2026-01-05 10:37:09 +07:00
try {
2026-04-28 17:28:57 +07:00
const response = await proxyAwareFetch (
2026-05-05 10:32:59 +07:00
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist" ,
2026-01-05 10:37:09 +07:00
{
2026-05-05 10:32:59 +07:00
method : "POST" ,
2026-01-05 10:37:09 +07:00
headers : {
Authorization : `Bearer ${ accessToken } ` ,
2026-05-05 10:32:59 +07:00
"Content-Type" : "application/json" ,
2026-01-05 10:37:09 +07:00
},
2026-05-05 10:32:59 +07:00
body : JSON . stringify ({
2026-05-17 22:25:33 +00:00
metadata : CLIENT_METADATA ,
2026-05-05 10:32:59 +07:00
}),
signal : controller . signal ,
2026-04-28 17:28:57 +07:00
},
proxyOptions
2026-01-05 10:37:09 +07:00
);
2026-05-05 10:32:59 +07:00
if ( ! response . ok ) return null ;
return await response . json ();
} catch {
return null ;
} finally {
clearTimeout ( timeoutId );
2026-01-05 10:37:09 +07:00
}
}
/**
* Antigravity Usage - Fetch quota from Google Cloud Code API
*/
2026-04-28 17:28:57 +07:00
async function getAntigravityUsage ( accessToken , providerSpecificData , proxyOptions = null ) {
2026-01-05 10:37:09 +07:00
try {
2026-03-14 09:37:29 +07:00
// Fetch subscription info once — reuse for both projectId and plan
2026-04-28 17:28:57 +07:00
const subscriptionInfo = await getAntigravitySubscriptionInfo ( accessToken , proxyOptions );
2026-03-14 09:37:29 +07:00
const projectId = subscriptionInfo ? . cloudaicompanionProject || null ;
2026-02-20 14:44:29 +07:00
2026-02-28 12:10:55 +07:00
// Fetch quota data with timeout
const controller = new AbortController ();
const timeoutId = setTimeout (() => controller . abort (), 10000 ); // 10s timeout
2026-03-14 09:37:29 +07:00
let response ;
try {
2026-04-28 17:28:57 +07:00
response = await proxyAwareFetch ( ANTIGRAVITY_CONFIG . quotaApiUrl , {
2026-03-14 09:37:29 +07:00
method : "POST" ,
headers : {
"Authorization" : `Bearer ${ accessToken } ` ,
"User-Agent" : ANTIGRAVITY_CONFIG . userAgent ,
"Content-Type" : "application/json" ,
"X-Client-Name" : "antigravity" ,
"X-Client-Version" : "1.107.0" ,
"x-request-source" : "local" , // MITM bypass
},
body : JSON . stringify ({
...( projectId ? { project : projectId } : {})
}),
signal : controller . signal ,
2026-04-28 17:28:57 +07:00
}, proxyOptions );
2026-03-14 09:37:29 +07:00
} finally {
clearTimeout ( timeoutId );
}
2026-01-05 10:37:09 +07:00
if ( response . status === 403 ) {
2026-02-25 11:40:50 +07:00
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 : {}
};
2026-01-05 10:37:09 +07:00
}
if ( ! response . ok ) {
throw new Error ( `Antigravity API error: ${ response . status } ` );
}
const data = await response . json ();
const quotas = {};
2026-02-20 14:44:29 +07:00
2026-02-05 18:38:50 +07:00
// Parse model quotas (inspired by vscode-antigravity-cockpit)
2026-01-05 10:37:09 +07:00
if ( data . models ) {
2026-02-05 18:38:50 +07:00
// Filter only recommended/important models (must match PROVIDER_MODELS ag ids)
const importantModels = [
2026-05-23 07:56:10 +05:30
'gemini-3-flash-agent' ,
'gemini-3.5-flash-low' ,
'gemini-pro-agent' ,
2026-02-22 15:20:24 +07:00
'gemini-3.1-pro-low' ,
2026-05-23 07:56:10 +05:30
'claude-sonnet-4-6' ,
'claude-opus-4-6-thinking' ,
2026-02-22 15:20:24 +07:00
'gpt-oss-120b-medium' ,
2026-05-23 07:56:10 +05:30
'gemini-3-flash' ,
2026-02-05 18:38:50 +07:00
];
2026-02-20 14:44:29 +07:00
2026-02-05 18:38:50 +07:00
for ( const [ modelKey , info ] of Object . entries ( data . models )) {
// Skip models without quota info
if ( ! info . quotaInfo ) {
continue ;
2026-01-05 10:37:09 +07:00
}
2026-02-20 14:44:29 +07:00
2026-02-05 18:38:50 +07:00
// Skip internal models and non-important models
if ( info . isInternal || ! importantModels . includes ( modelKey )) {
continue ;
}
2026-02-20 14:44:29 +07:00
2026-02-05 18:38:50 +07:00
const remainingFraction = info . quotaInfo . remainingFraction || 0 ;
const remainingPercentage = remainingFraction * 100 ;
2026-02-20 14:44:29 +07:00
2026-02-05 18:38:50 +07:00
// Convert percentage to used/total for UI compatibility
const total = 1000 ; // Normalized base
const remaining = Math . round ( total * remainingFraction );
const used = total - remaining ;
2026-02-20 14:44:29 +07:00
2026-02-05 18:38:50 +07:00
// Use modelKey as key (matches PROVIDER_MODELS id)
quotas [ modelKey ] = {
used ,
total ,
2026-02-06 11:23:08 +07:00
resetAt : parseResetTime ( info . quotaInfo . resetTime ),
2026-02-05 18:38:50 +07:00
remainingPercentage ,
unlimited : false ,
displayName : info . displayName || modelKey ,
};
2026-01-05 10:37:09 +07:00
}
}
return {
plan : subscriptionInfo ? . currentTier ? . name || "Unknown" ,
quotas ,
subscriptionInfo ,
};
} catch ( error ) {
2026-02-28 12:10:55 +07:00
console . error ( "[Antigravity Usage] Error:" , error . message , error . cause );
2026-01-05 10:37:09 +07:00
return { message : `Antigravity error: ${ error . message } ` };
}
}
/**
* Get Antigravity project ID from subscription info
*/
async function getAntigravityProjectId ( accessToken ) {
try {
const info = await getAntigravitySubscriptionInfo ( accessToken );
return info ? . cloudaicompanionProject || null ;
} catch {
return null ;
}
}
/**
* Get Antigravity subscription info
*/
2026-04-28 17:28:57 +07:00
async function getAntigravitySubscriptionInfo ( accessToken , proxyOptions = null ) {
2026-03-14 09:37:29 +07:00
const controller = new AbortController ();
const timeoutId = setTimeout (() => controller . abort (), 10000 ); // 10s timeout
2026-01-05 10:37:09 +07:00
try {
2026-04-28 17:28:57 +07:00
const response = await proxyAwareFetch ( ANTIGRAVITY_CONFIG . loadProjectApiUrl , {
2026-01-05 10:37:09 +07:00
method : "POST" ,
headers : {
"Authorization" : `Bearer ${ accessToken } ` ,
"User-Agent" : ANTIGRAVITY_CONFIG . userAgent ,
"Content-Type" : "application/json" ,
2026-02-28 12:10:55 +07:00
"x-request-source" : "local" , // MITM bypass
2026-01-05 10:37:09 +07:00
},
2026-02-20 14:44:29 +07:00
body : JSON . stringify ({ metadata : CLIENT_METADATA , mode : 1 }),
2026-02-28 12:10:55 +07:00
signal : controller . signal ,
2026-04-28 17:28:57 +07:00
}, proxyOptions );
2026-01-05 10:37:09 +07:00
if ( ! response . ok ) return null ;
return await response . json ();
2026-02-28 12:10:55 +07:00
} catch ( error ) {
console . error ( "[Antigravity Subscription] Error:" , error . message );
2026-01-05 10:37:09 +07:00
return null ;
2026-03-14 09:37:29 +07:00
} finally {
clearTimeout ( timeoutId );
2026-01-05 10:37:09 +07:00
}
}
/**
2026-03-12 09:42:17 +07:00
* Claude Usage - Primary: OAuth endpoint, Fallback: legacy settings/org endpoint
2026-01-05 10:37:09 +07:00
*/
2026-04-28 17:28:57 +07:00
async function getClaudeUsage ( accessToken , proxyOptions = null ) {
2026-01-05 10:37:09 +07:00
try {
2026-03-12 09:42:17 +07:00
// Primary: OAuth usage endpoint (Claude Code consumer OAuth tokens)
2026-04-28 17:28:57 +07:00
const oauthResponse = await proxyAwareFetch ( CLAUDE_CONFIG . oauthUsageUrl , {
2026-01-05 10:37:09 +07:00
method : "GET" ,
headers : {
"Authorization" : `Bearer ${ accessToken } ` ,
2026-03-12 09:42:17 +07:00
"anthropic-beta" : "oauth-2025-04-20" ,
"anthropic-version" : CLAUDE_CONFIG . apiVersion ,
},
2026-04-28 17:28:57 +07:00
}, proxyOptions );
2026-03-12 09:42:17 +07:00
if ( oauthResponse . ok ) {
const data = await oauthResponse . json ();
const quotas = {};
// utilization = % USED (e.g. 87 means 87% used, 13% remaining)
const hasUtilization = ( window ) =>
window && typeof window === "object" && typeof window . utilization === "number" ;
const createQuotaObject = ( window ) => {
const used = window . utilization ;
const remaining = Math . max ( 0 , 100 - used );
return {
used ,
total : 100 ,
remaining ,
remainingPercentage : remaining ,
resetAt : parseResetTime ( window . resets_at ),
unlimited : false ,
};
};
if ( hasUtilization ( data . five_hour )) {
quotas [ "session (5h)" ] = createQuotaObject ( data . five_hour );
}
if ( hasUtilization ( data . seven_day )) {
quotas [ "weekly (7d)" ] = createQuotaObject ( data . seven_day );
}
// Parse model-specific weekly windows (e.g. seven_day_sonnet, seven_day_opus)
for ( const [ key , value ] of Object . entries ( data )) {
if ( key . startsWith ( "seven_day_" ) && key !== "seven_day" && hasUtilization ( value )) {
const modelName = key . replace ( "seven_day_" , "" );
quotas [ `weekly ${ modelName } (7d)` ] = createQuotaObject ( value );
}
}
return {
plan : "Claude Code" ,
extraUsage : data . extra_usage ?? null ,
quotas ,
};
}
// Fallback: legacy settings + org usage endpoint
console . warn ( `[Claude Usage] OAuth endpoint returned ${ oauthResponse . status } , falling back to legacy` );
2026-04-28 17:28:57 +07:00
return await getClaudeUsageLegacy ( accessToken , proxyOptions );
2026-03-12 09:42:17 +07:00
} catch ( error ) {
return { message : `Claude connected. Unable to fetch usage: ${ error . message } ` };
}
}
/**
* Legacy Claude usage for API key / org admin users
*/
2026-04-28 17:28:57 +07:00
async function getClaudeUsageLegacy ( accessToken , proxyOptions = null ) {
2026-03-12 09:42:17 +07:00
try {
2026-04-28 17:28:57 +07:00
const settingsResponse = await proxyAwareFetch ( CLAUDE_CONFIG . settingsUrl , {
2026-03-12 09:42:17 +07:00
method : "GET" ,
headers : {
"Authorization" : `Bearer ${ accessToken } ` ,
"anthropic-version" : CLAUDE_CONFIG . apiVersion ,
2026-01-05 10:37:09 +07:00
},
2026-04-28 17:28:57 +07:00
}, proxyOptions );
2026-01-05 10:37:09 +07:00
if ( settingsResponse . ok ) {
const settings = await settingsResponse . json ();
2026-02-20 14:44:29 +07:00
2026-01-05 10:37:09 +07:00
if ( settings . organization_id ) {
2026-04-28 17:28:57 +07:00
const usageResponse = await proxyAwareFetch (
2026-03-12 09:42:17 +07:00
CLAUDE_CONFIG . usageUrl . replace ( "{org_id}" , settings . organization_id ),
2026-01-05 10:37:09 +07:00
{
method : "GET" ,
headers : {
"Authorization" : `Bearer ${ accessToken } ` ,
2026-03-12 09:42:17 +07:00
"anthropic-version" : CLAUDE_CONFIG . apiVersion ,
2026-01-05 10:37:09 +07:00
},
2026-04-28 17:28:57 +07:00
},
proxyOptions
2026-01-05 10:37:09 +07:00
);
if ( usageResponse . ok ) {
const usage = await usageResponse . json ();
return {
plan : settings . plan || "Unknown" ,
organization : settings . organization_name ,
quotas : usage ,
};
}
}
return {
plan : settings . plan || "Unknown" ,
organization : settings . organization_name ,
message : "Claude connected. Usage details require admin access." ,
};
}
return { message : "Claude connected. Usage API requires admin permissions." };
} catch ( error ) {
return { message : `Claude connected. Unable to fetch usage: ${ error . message } ` };
}
}
/**
* Codex (OpenAI) Usage - Fetch from ChatGPT backend API
*/
2026-05-03 14:57:33 +07:00
function toFiniteNumber ( value , fallback = 0 ) {
if ( typeof value === "number" && Number . isFinite ( value )) return value ;
if ( typeof value === "string" && value . trim ()) {
const parsed = Number ( value );
if ( Number . isFinite ( parsed )) return parsed ;
}
return fallback ;
}
function getCodexRateLimitBody ( snapshot ) {
if ( ! snapshot || typeof snapshot !== "object" || Array . isArray ( snapshot )) return null ;
return snapshot . rate_limit && typeof snapshot . rate_limit === "object"
? snapshot . rate_limit
: snapshot ;
}
function formatCodexWindow ( window ) {
const used = Math . max ( 0 , Math . min ( 100 , toFiniteNumber ( window ? . used_percent ?? window ? . percent_used , 0 )));
return {
used ,
total : 100 ,
remaining : Math . max ( 0 , 100 - used ),
resetAt : parseResetTime ( window ? . reset_at ?? window ? . resets_at ?? window ? . resetAt ?? null ),
unlimited : false ,
};
}
function appendCodexQuotaWindows ( quotas , prefix , snapshot ) {
const rateLimit = getCodexRateLimitBody ( snapshot );
if ( ! rateLimit ) return false ;
const primary = rateLimit . primary_window || rateLimit . primary || snapshot . primary_window || snapshot . primary ;
const secondary = rateLimit . secondary_window || rateLimit . secondary || snapshot . secondary_window || snapshot . secondary ;
let added = false ;
if ( primary ) {
quotas [ prefix ? ` ${ prefix } _session` : "session" ] = formatCodexWindow ( primary );
added = true ;
}
if ( secondary ) {
quotas [ prefix ? ` ${ prefix } _weekly` : "weekly" ] = formatCodexWindow ( secondary );
added = true ;
}
return added ;
}
function getCodexReviewRateLimit ( data ) {
if ( data . code_review_rate_limit || data . review_rate_limit ) {
return data . code_review_rate_limit || data . review_rate_limit ;
}
const byLimitId = data . rate_limits_by_limit_id ;
if ( byLimitId && typeof byLimitId === "object" && ! Array . isArray ( byLimitId )) {
return byLimitId . code_review || byLimitId . codex_review || byLimitId . review || null ;
}
const additional = Array . isArray ( data . additional_rate_limits ) ? data . additional_rate_limits : [];
return additional . find (( entry ) => {
const id = String ( entry ? . limit_name || entry ? . metered_feature || entry ? . id || "" ). toLowerCase ();
return id === "code_review" || id === "codex_review" || id === "review" || id . includes ( "review" );
}) || null ;
}
2026-04-28 17:28:57 +07:00
async function getCodexUsage ( accessToken , proxyOptions = null ) {
2026-01-05 10:37:09 +07:00
try {
2026-04-28 17:28:57 +07:00
const response = await proxyAwareFetch ( CODEX_CONFIG . usageUrl , {
2026-01-05 10:37:09 +07:00
method : "GET" ,
headers : {
"Authorization" : `Bearer ${ accessToken } ` ,
"Accept" : "application/json" ,
},
2026-04-28 17:28:57 +07:00
}, proxyOptions );
2026-01-05 10:37:09 +07:00
if ( ! response . ok ) {
2026-04-15 11:44:27 +07:00
return { message : `Codex connected. Usage API temporarily unavailable ( ${ response . status } ).` };
2026-01-05 10:37:09 +07:00
}
const data = await response . json ();
2026-05-03 14:57:33 +07:00
const normalRateLimit = data . rate_limit || data . rate_limits || data . rate_limits_by_limit_id ? . codex || {};
const reviewRateLimit = getCodexReviewRateLimit ( data );
const quotas = {};
2026-02-20 14:44:29 +07:00
2026-05-03 14:57:33 +07:00
appendCodexQuotaWindows ( quotas , "" , normalRateLimit );
appendCodexQuotaWindows ( quotas , "review" , reviewRateLimit );
2026-01-05 10:37:09 +07:00
return {
2026-05-03 14:57:33 +07:00
plan : data . plan_type || data . summary ? . plan || "unknown" ,
limitReached : getCodexRateLimitBody ( normalRateLimit ) ? . limit_reached || false ,
reviewLimitReached : getCodexRateLimitBody ( reviewRateLimit ) ? . limit_reached || false ,
quotas ,
2026-01-05 10:37:09 +07:00
};
} catch ( error ) {
throw new Error ( `Failed to fetch Codex usage: ${ error . message } ` );
}
}
2026-02-04 09:54:11 +07:00
/**
* Kiro (AWS CodeWhisperer) Usage
*/
2026-04-15 11:44:46 +07:00
function parseKiroQuotaData ( data ) {
const usageList = data . usageBreakdownList || [];
const quotaInfo = {};
const resetAt = parseResetTime ( data . nextDateReset || data . resetDate );
usageList . forEach (( breakdown ) => {
const resourceType = breakdown . resourceType ? . toLowerCase () || "unknown" ;
const used = breakdown . currentUsageWithPrecision || 0 ;
const total = breakdown . usageLimitWithPrecision || 0 ;
quotaInfo [ resourceType ] = {
used ,
total ,
remaining : total - used ,
resetAt ,
unlimited : false ,
};
// Add free trial if available
if ( breakdown . freeTrialInfo ) {
const freeUsed = breakdown . freeTrialInfo . currentUsageWithPrecision || 0 ;
const freeTotal = breakdown . freeTrialInfo . usageLimitWithPrecision || 0 ;
quotaInfo [ ` ${ resourceType } _freetrial` ] = {
used : freeUsed ,
total : freeTotal ,
remaining : freeTotal - freeUsed ,
resetAt : parseResetTime ( breakdown . freeTrialInfo . freeTrialExpiry || resetAt ),
unlimited : false ,
};
}
});
return {
plan : data . subscriptionInfo ? . subscriptionTitle || "Kiro" ,
quotas : quotaInfo ,
};
}
2026-04-28 17:28:57 +07:00
async function getKiroUsage ( accessToken , providerSpecificData , proxyOptions = null ) {
2026-03-06 00:21:27 +07:00
// Default profileArn fallback
const DEFAULT_PROFILE_ARN = "arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX" ;
const profileArn = providerSpecificData ? . profileArn || DEFAULT_PROFILE_ARN ;
2026-04-15 11:44:46 +07:00
const authMethod = providerSpecificData ? . authMethod || "builder-id" ;
2026-02-04 09:54:11 +07:00
2026-04-15 11:44:46 +07:00
const getUsageParams = new URLSearchParams ({
isEmailRequired : "true" ,
origin : "AI_EDITOR" ,
resourceType : "AGENTIC_REQUEST" ,
});
2026-02-04 09:54:11 +07:00
2026-04-15 11:44:46 +07:00
// For compatibility, try multiple known Kiro usage endpoints
const attempts = [
{
name : "codewhisperer-get" ,
2026-04-28 17:28:57 +07:00
run : async () => proxyAwareFetch (
2026-04-15 11:44:46 +07:00
`https://codewhisperer.us-east-1.amazonaws.com/getUsageLimits? ${ getUsageParams . toString () } ` ,
{
method : "GET" ,
headers : {
"Authorization" : `Bearer ${ accessToken } ` ,
"Accept" : "application/json" ,
"x-amz-user-agent" : "aws-sdk-js/1.0.0 KiroIDE" ,
"user-agent" : "aws-sdk-js/1.0.0 KiroIDE" ,
},
},
2026-04-28 17:28:57 +07:00
proxyOptions
2026-04-15 11:44:46 +07:00
),
},
{
name : "codewhisperer-post" ,
2026-04-28 17:28:57 +07:00
run : async () => proxyAwareFetch ( "https://codewhisperer.us-east-1.amazonaws.com" , {
2026-04-15 11:44:46 +07:00
method : "POST" ,
2026-03-06 00:21:27 +07:00
headers : {
"Authorization" : `Bearer ${ accessToken } ` ,
2026-04-15 11:44:46 +07:00
"Content-Type" : "application/x-amz-json-1.0" ,
"x-amz-target" : "AmazonCodeWhispererService.GetUsageLimits" ,
2026-03-06 00:21:27 +07:00
"Accept" : "application/json" ,
},
2026-04-15 11:44:46 +07:00
body : JSON . stringify ({
origin : "AI_EDITOR" ,
profileArn ,
resourceType : "AGENTIC_REQUEST" ,
}),
2026-04-28 17:28:57 +07:00
}, proxyOptions ),
2026-04-15 11:44:46 +07:00
},
{
name : "q-get" ,
run : async () => {
const params = new URLSearchParams ({
origin : "AI_EDITOR" ,
profileArn ,
resourceType : "AGENTIC_REQUEST" ,
});
2026-04-28 17:28:57 +07:00
return proxyAwareFetch ( `https://q.us-east-1.amazonaws.com/getUsageLimits? ${ params } ` , {
2026-04-15 11:44:46 +07:00
method : "GET" ,
headers : {
"Authorization" : `Bearer ${ accessToken } ` ,
"Accept" : "application/json" ,
},
2026-04-28 17:28:57 +07:00
}, proxyOptions );
2026-04-15 11:44:46 +07:00
},
},
];
2026-03-06 00:21:27 +07:00
2026-04-15 11:44:46 +07:00
let sawAuthError = false ;
const errors = [];
for ( const attempt of attempts ) {
try {
const response = await attempt . run ();
if ( ! response . ok ) {
const errorText = await response . text (). catch (() => "" );
if ( response . status === 401 || response . status === 403 ) {
sawAuthError = true ;
}
errors . push ( ` ${ attempt . name } : ${ response . status }${ errorText ? `: ${ errorText } ` : "" } ` );
continue ;
2026-03-06 00:21:27 +07:00
}
2026-04-15 11:44:46 +07:00
const data = await response . json ();
return parseKiroQuotaData ( data );
} catch ( error ) {
errors . push ( ` ${ attempt . name } : ${ error . message } ` );
2026-03-06 00:21:27 +07:00
}
2026-02-04 09:54:11 +07:00
}
2026-04-15 11:44:46 +07:00
if ( sawAuthError && authMethod === "idc" ) {
return {
message : "Kiro quota API is unavailable for the current AWS IAM Identity Center session. Chat may still work. If this persists after renewing your session, reconnect Kiro." ,
quotas : {},
};
}
2026-04-17 01:24:35 -04:00
// Social auth (Google/GitHub) - these use a different token format that may not work with AWS CodeWhisperer quota APIs
if ( sawAuthError && ( authMethod === "google" || authMethod === "github" )) {
return {
message : "Kiro quota API authentication expired. Chat may still work." ,
quotas : {},
};
}
2026-04-15 11:44:46 +07:00
if ( sawAuthError ) {
return {
message : "Kiro quota API rejected the current token. Chat may still work." ,
quotas : {},
};
}
const fallbackMessage =
errors . length > 0
? `Unable to fetch Kiro usage right now. ( ${ errors [ errors . length - 1 ] } )`
: "Unable to fetch Kiro usage right now." ;
return {
message : fallbackMessage ,
quotas : {},
};
2026-02-04 09:54:11 +07:00
}
2026-01-05 10:37:09 +07:00
/**
* Qwen Usage
*/
async function getQwenUsage ( accessToken , providerSpecificData ) {
try {
const resourceUrl = providerSpecificData ? . resourceUrl ;
if ( ! resourceUrl ) {
return { message : "Qwen connected. No resource URL available." };
}
// Qwen may have usage endpoint at resource URL
return { message : "Qwen connected. Usage tracked per request." };
} catch ( error ) {
return { message : "Unable to fetch Qwen usage." };
}
}
/**
* iFlow Usage
*/
async function getIflowUsage ( accessToken ) {
try {
// iFlow may have usage endpoint
return { message : "iFlow connected. Usage tracked per request." };
} catch ( error ) {
return { message : "Unable to fetch iFlow usage." };
}
}
2026-04-21 23:32:28 -04:00
/**
* Ollama Cloud Usage
* Ollama Cloud uses an API key from ollama.com/settings/keys
* and has no public usage API — free tier has light usage limits (resets every 5h & 7d).
* This returns an informational message with the plan details.
*/
async function getOllamaUsage ( accessToken , providerSpecificData ) {
try {
// Ollama Cloud does not expose a public quota/usage API.
// The provider is configured as noAuth with a notice explaining limits.
// We return a graceful message so the UI shows a friendly state instead of an error.
const plan = providerSpecificData ? . plan || "Free" ;
return {
plan ,
message : "Ollama Cloud uses a free tier with light usage limits (resets every 5h & 7d). For detailed usage tracking, visit ollama.com/settings/keys." ,
quotas : [],
};
} catch ( error ) {
return { message : "Unable to fetch Ollama Cloud usage." };
}
}
2026-05-05 10:32:59 +07:00
/**
* GLM Coding Plan usage (international + China regions)
*/
async function getGlmUsage ( apiKey , provider , proxyOptions = null ) {
if ( ! apiKey ) {
return { message : "GLM API key not available." };
}
const region = provider === "glm-cn" ? "china" : "international" ;
const quotaUrl = GLM_QUOTA_URLS [ region ];
try {
const response = await proxyAwareFetch ( quotaUrl , {
headers : {
Authorization : `Bearer ${ apiKey } ` ,
Accept : "application/json" ,
},
}, proxyOptions );
if ( ! response . ok ) {
if ( response . status === 401 ) {
return { message : "GLM API key invalid or expired." };
}
return { message : `GLM quota API error ( ${ response . status } ).` };
}
const json = await response . json ();
const data = json ? . data && typeof json . data === "object" ? json . data : {};
const limits = Array . isArray ( data . limits ) ? data . limits : [];
const quotas = {};
for ( const limit of limits ) {
if ( ! limit || limit . type !== "TOKENS_LIMIT" ) continue ;
const usedPercent = Number ( limit . percentage ) || 0 ;
const resetMs = Number ( limit . nextResetTime ) || 0 ;
const remaining = Math . max ( 0 , 100 - usedPercent );
quotas [ "session" ] = {
used : usedPercent ,
total : 100 ,
remaining ,
remainingPercentage : remaining ,
resetAt : resetMs > 0 ? new Date ( resetMs ). toISOString () : null ,
unlimited : false ,
};
}
const levelRaw = typeof data . level === "string" ? data . level : "" ;
const plan = levelRaw
? levelRaw . charAt ( 0 ). toUpperCase () + levelRaw . slice ( 1 ). toLowerCase ()
: "Unknown" ;
return { plan , quotas };
} catch ( error ) {
return { message : `GLM error: ${ error . message } ` };
}
}
// ── MiniMax helpers ──────────────────────────────────────────────────────
function getMiniMaxField ( model , snakeKey , camelKey ) {
if ( ! model || typeof model !== "object" ) return null ;
return model [ snakeKey ] ?? model [ camelKey ] ?? null ;
}
2026-05-13 15:34:10 +07:00
function getMiniMaxModelName ( model ) {
return String ( getMiniMaxField ( model , "model_name" , "modelName" ) || "" ). trim ();
}
function formatMiniMaxQuotaName ( model ) {
const rawName = getMiniMaxModelName ( model );
if ( ! rawName ) return "MiniMax" ;
return rawName
. replace ( /[_-]+/g , " " )
. replace ( /\s+/g , " " )
. trim ()
. replace ( /\b\w/g , ( ch ) => ch . toUpperCase ())
. replace ( /\bTo\b/g , "to" )
. replace ( /\bTts\b/g , "TTS" )
. replace ( /\bHd\b/g , "HD" );
}
2026-05-05 10:32:59 +07:00
function getMiniMaxSessionTotal ( model ) {
return Math . max ( 0 , Number ( getMiniMaxField ( model , "current_interval_total_count" , "currentIntervalTotalCount" )) || 0 );
}
function getMiniMaxWeeklyTotal ( model ) {
return Math . max ( 0 , Number ( getMiniMaxField ( model , "current_weekly_total_count" , "currentWeeklyTotalCount" )) || 0 );
}
2026-05-13 15:34:10 +07:00
function hasMiniMaxQuota ( model ) {
return getMiniMaxSessionTotal ( model ) > 0 || getMiniMaxWeeklyTotal ( model ) > 0 ;
2026-05-05 10:32:59 +07:00
}
function getMiniMaxResetAt ( model , capturedAtMs , remainsSnake , remainsCamel , endSnake , endCamel ) {
const remainsMs = Number ( getMiniMaxField ( model , remainsSnake , remainsCamel )) || 0 ;
if ( remainsMs > 0 ) return new Date ( capturedAtMs + remainsMs ). toISOString ();
return parseResetTime ( getMiniMaxField ( model , endSnake , endCamel ));
}
function buildMiniMaxQuota ( total , count , resetAt , countMeansRemaining ) {
const safeTotal = Math . max ( 0 , total );
const used = countMeansRemaining ? Math . max ( safeTotal - count , 0 ) : Math . min ( Math . max ( 0 , count ), safeTotal );
const remaining = Math . max ( safeTotal - used , 0 );
return {
used ,
total : safeTotal ,
remaining ,
remainingPercentage : safeTotal > 0 ? Math . max ( 0 , Math . min ( 100 , ( remaining / safeTotal ) * 100 )) : 0 ,
resetAt ,
unlimited : false ,
};
}
2026-05-13 15:34:10 +07:00
function addMiniMaxQuota ( quotas , key , model , getTotal , countSnake , countCamel , resetArgs , countMeansRemaining ) {
const total = getTotal ( model );
if ( total <= 0 ) return ;
const count = Math . max ( 0 , Number ( getMiniMaxField ( model , countSnake , countCamel )) || 0 );
quotas [ key ] = buildMiniMaxQuota (
total ,
count ,
getMiniMaxResetAt ( model , ... resetArgs ),
countMeansRemaining
);
}
2026-05-05 10:32:59 +07:00
/**
* MiniMax Token Plan / Coding Plan usage
*/
async function getMiniMaxUsage ( apiKey , provider , proxyOptions = null ) {
if ( ! apiKey ) {
return { message : "MiniMax API key not available." };
}
const usageUrls = MINIMAX_USAGE_URLS [ provider ] || [];
let lastErrorMessage = "" ;
for ( let index = 0 ; index < usageUrls . length ; index += 1 ) {
const usageUrl = usageUrls [ index ];
const canFallback = index < usageUrls . length - 1 ;
try {
const response = await proxyAwareFetch ( usageUrl , {
method : "GET" ,
headers : {
Authorization : `Bearer ${ apiKey } ` ,
Accept : "application/json" ,
"Content-Type" : "application/json" ,
},
}, proxyOptions );
const rawText = await response . text ();
let payload = {};
if ( rawText ) {
try { payload = JSON . parse ( rawText ); } catch { payload = {}; }
}
const baseResp = ( payload ? . base_resp ?? payload ? . baseResp ) || {};
const apiStatusCode = Number ( baseResp . status_code ?? baseResp . statusCode ) || 0 ;
const apiStatusMessage = String ( baseResp . status_msg ?? baseResp . statusMsg ?? "" ). trim ();
const combined = ` ${ apiStatusMessage } ${ rawText } ` . trim ();
const authLike = /token plan|coding plan|invalid api key|invalid key|unauthorized|inactive/i ;
if ( response . status === 401 || response . status === 403 || apiStatusCode === 1004 || authLike . test ( combined )) {
return { message : "MiniMax API key invalid or inactive. Use an active Token/Coding Plan key." };
}
if ( ! response . ok ) {
lastErrorMessage = `MiniMax usage endpoint error ( ${ response . status } )` ;
if (( response . status === 404 || response . status === 405 || response . status >= 500 ) && canFallback ) continue ;
return { message : `MiniMax connected. ${ lastErrorMessage } ` };
}
if ( apiStatusCode !== 0 ) {
return { message : `MiniMax connected. ${ apiStatusMessage || "Upstream quota API error" } ` };
}
const modelRemains = payload ? . model_remains ?? payload ? . modelRemains ;
const allModels = Array . isArray ( modelRemains ) ? modelRemains : [];
2026-05-13 15:34:10 +07:00
const quotaModels = allModels . filter ( hasMiniMaxQuota );
2026-05-05 10:32:59 +07:00
2026-05-13 15:34:10 +07:00
if ( quotaModels . length === 0 ) {
return { message : "MiniMax connected. No quota data was returned." };
2026-05-05 10:32:59 +07:00
}
const capturedAtMs = Date . now ();
const countMeansRemaining = usageUrl . includes ( "/coding_plan/remains" );
const quotas = {};
2026-05-13 15:34:10 +07:00
for ( const model of quotaModels ) {
const displayName = formatMiniMaxQuotaName ( model );
addMiniMaxQuota (
quotas ,
` ${ displayName } (5h)` ,
model ,
getMiniMaxSessionTotal ,
"current_interval_usage_count" ,
"currentIntervalUsageCount" ,
[ capturedAtMs , "remains_time" , "remainsTime" , "end_time" , "endTime" ],
2026-05-05 10:32:59 +07:00
countMeansRemaining
);
2026-05-13 15:34:10 +07:00
addMiniMaxQuota (
quotas ,
` ${ displayName } (7d)` ,
model ,
getMiniMaxWeeklyTotal ,
"current_weekly_usage_count" ,
"currentWeeklyUsageCount" ,
[ capturedAtMs , "weekly_remains_time" , "weeklyRemainsTime" , "weekly_end_time" , "weeklyEndTime" ],
2026-05-05 10:32:59 +07:00
countMeansRemaining
);
}
if ( Object . keys ( quotas ). length === 0 ) {
return { message : "MiniMax connected. Unable to extract quota usage." };
}
return { quotas };
} catch ( error ) {
lastErrorMessage = error . message ;
if ( ! canFallback ) break ;
}
}
return { message : lastErrorMessage ? `MiniMax connected. Unable to fetch usage: ${ lastErrorMessage } ` : "MiniMax connected. Unable to fetch usage." };
}
2026-05-23 16:31:50 +09:00
async function getQoderUsage ( accessToken , proxyOptions = null ) {
if ( ! accessToken ) {
return { message : "Qoder usage unavailable: no access token" };
}
try {
const response = await proxyAwareFetch (
"https://openapi.qoder.sh/api/v2/quota/usage" ,
{
method : "GET" ,
headers : {
Authorization : `Bearer ${ accessToken } ` ,
Accept : "application/json" ,
},
},
proxyOptions ,
);
if ( ! response . ok ) {
return { message : `Qoder connected. Usage fetch returned ${ response . status } .` };
}
const body = await response . json (). catch (() => null );
if ( ! body ) {
return { message : "Qoder connected. Usage response was not JSON." };
}
2026-05-23 22:50:53 +09:00
// Quota records live under `quotas`; scalar metadata
// (totalUsagePercentage, isQuotaExceeded, expiresAt) are surfaced as
// siblings so the dashboard parser doesn't try to render them as rows.
2026-05-23 16:31:50 +09:00
const userQuota = body . userQuota || {};
const orgQuota = body . orgResourcePackage || {};
2026-05-23 22:50:53 +09:00
// Qoder publishes a single absolute reset timestamp (`expiresAt` in ms);
// surface it on every quota record as ISO so the table can render
// "resets at" alongside used/total.
const expiresAtMs = Number . isFinite ( Number ( body . expiresAt )) && Number ( body . expiresAt ) > 0
? Number ( body . expiresAt )
: null ;
const resetAt = expiresAtMs ? new Date ( expiresAtMs ). toISOString () : null ;
2026-05-23 16:31:50 +09:00
const quotas = {
user : {
total : Number ( userQuota . total ) || 0 ,
used : Number ( userQuota . used ) || 0 ,
remaining : Number ( userQuota . remaining ) || 0 ,
unit : userQuota . unit || "credits" ,
2026-05-23 22:50:53 +09:00
resetAt ,
2026-05-23 16:31:50 +09:00
},
organization : {
total : Number ( orgQuota . total ) || 0 ,
used : Number ( orgQuota . used ) || 0 ,
remaining : Number ( orgQuota . remaining ) || 0 ,
unit : orgQuota . unit || "credits" ,
2026-05-23 22:50:53 +09:00
resetAt ,
2026-05-23 16:31:50 +09:00
},
2026-05-23 22:50:53 +09:00
};
return {
quotas ,
2026-05-23 16:31:50 +09:00
totalUsagePercentage : Number ( body . totalUsagePercentage ) || 0 ,
isQuotaExceeded : !! body . isQuotaExceeded ,
2026-05-23 22:50:53 +09:00
expiresAt : expiresAtMs ,
2026-05-23 16:31:50 +09:00
};
} catch ( error ) {
return { message : `Qoder connected. Unable to fetch usage: ${ error . message } ` };
}
}