| |
| |
| |
|
|
| import { CLIENT_METADATA, getPlatformUserAgent } from "../config/appConstants.js"; |
| import { proxyAwareFetch } from "../utils/proxyFetch.js"; |
| import { resolveDefaultProfileArn } from "../config/kiroConstants.js"; |
|
|
| |
| const GITHUB_CONFIG = { |
| apiVersion: "2022-11-28", |
| userAgent: "GitHubCopilotChat/0.26.7", |
| }; |
|
|
| |
| const GLM_QUOTA_URLS = { |
| international: "https://api.z.ai/api/monitor/usage/quota/limit", |
| china: "https://open.bigmodel.cn/api/monitor/usage/quota/limit", |
| }; |
|
|
| |
| 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", |
| ], |
| }; |
|
|
| |
| |
| |
| const VERCEL_AI_GATEWAY_CREDITS_URL = "https://ai-gateway.vercel.sh/v1/credits"; |
|
|
| |
| 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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", |
| userAgent: getPlatformUserAgent(), |
| }; |
|
|
| |
| const CODEX_CONFIG = { |
| usageUrl: "https://chatgpt.com/backend-api/wham/usage", |
| }; |
|
|
| |
| const CLAUDE_CONFIG = { |
| oauthUsageUrl: "https://api.anthropic.com/api/oauth/usage", |
| usageUrl: "https://api.anthropic.com/v1/organizations/{org_id}/usage", |
| settingsUrl: "https://api.anthropic.com/v1/settings", |
| apiVersion: "2023-06-01", |
| }; |
|
|
| |
| |
| |
| |
| |
| export async function getUsageForProvider(connection, proxyOptions = null) { |
| const { provider, accessToken, apiKey, providerSpecificData, projectId } = connection; |
| const providerDataWithProjectId = { |
| ...(providerSpecificData || {}), |
| ...(projectId ? { projectId } : {}), |
| }; |
|
|
| switch (provider) { |
| case "github": |
| return await getGitHubUsage(accessToken, providerSpecificData, proxyOptions); |
| case "gemini-cli": |
| return await getGeminiUsage(accessToken, providerDataWithProjectId, proxyOptions); |
| case "antigravity": |
| return await getAntigravityUsage(accessToken, providerSpecificData, proxyOptions); |
| case "claude": |
| return await getClaudeUsage(accessToken, proxyOptions); |
| case "codex": |
| return await getCodexUsage(accessToken, proxyOptions); |
| case "kiro": |
| return await getKiroUsage(accessToken, providerSpecificData, proxyOptions); |
| case "qoder": |
| return await getQoderUsage(accessToken, proxyOptions); |
| case "qwen": |
| return await getQwenUsage(accessToken, providerSpecificData); |
| case "iflow": |
| return await getIflowUsage(accessToken); |
| case "ollama": |
| return await getOllamaUsage(accessToken); |
| case "glm": |
| case "glm-cn": |
| return await getGlmUsage(apiKey, provider, proxyOptions); |
| case "minimax": |
| case "minimax-cn": |
| return await getMiniMaxUsage(apiKey, provider, proxyOptions); |
| case "vercel-ai-gateway": |
| return await getVercelAiGatewayUsage(apiKey, proxyOptions); |
| default: |
| return { message: `Usage API not implemented for ${provider}` }; |
| } |
| } |
|
|
| |
| |
| |
| |
| function parseResetTime(resetValue) { |
| if (!resetValue) return null; |
|
|
| try { |
| |
| if (resetValue instanceof Date) { |
| return resetValue.toISOString(); |
| } |
|
|
| |
| if (typeof resetValue === 'number') { |
| return new Date(resetValue < 1e12 ? resetValue * 1000 : resetValue).toISOString(); |
| } |
|
|
| |
| if (typeof resetValue === 'string') { |
| if (/^\d+$/.test(resetValue)) { |
| const timestamp = Number(resetValue); |
| return new Date(timestamp < 1e12 ? timestamp * 1000 : timestamp).toISOString(); |
| } |
| return new Date(resetValue).toISOString(); |
| } |
|
|
| return null; |
| } catch (error) { |
| console.warn(`Failed to parse reset time: ${resetValue}`, error); |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| |
| async function getGitHubUsage(accessToken, providerSpecificData, proxyOptions = null) { |
| try { |
| if (!accessToken) { |
| throw new Error("No GitHub access token available. Please re-authorize the connection."); |
| } |
|
|
| |
| const response = await proxyAwareFetch("https://api.github.com/copilot_internal/user", { |
| headers: { |
| "Authorization": `token ${accessToken}`, |
| "Accept": "application/json", |
| "X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion, |
| "User-Agent": GITHUB_CONFIG.userAgent, |
| "Editor-Version": "vscode/1.100.0", |
| "Editor-Plugin-Version": "copilot-chat/0.26.7", |
| }, |
| }, proxyOptions); |
|
|
| if (!response.ok) { |
| const error = await response.text(); |
| throw new Error(`GitHub API error: ${error}`); |
| } |
|
|
| const data = await response.json(); |
|
|
| |
| if (data.quota_snapshots) { |
| |
| const snapshots = data.quota_snapshots; |
| const resetAt = parseResetTime(data.quota_reset_date); |
|
|
| return { |
| plan: data.copilot_plan, |
| resetDate: data.quota_reset_date, |
| quotas: { |
| chat: { ...formatGitHubQuotaSnapshot(snapshots.chat), resetAt }, |
| completions: { ...formatGitHubQuotaSnapshot(snapshots.completions), resetAt }, |
| premium_interactions: { ...formatGitHubQuotaSnapshot(snapshots.premium_interactions), resetAt }, |
| }, |
| }; |
| } else if (data.monthly_quotas || data.limited_user_quotas) { |
| |
| const monthlyQuotas = data.monthly_quotas || {}; |
| const usedQuotas = data.limited_user_quotas || {}; |
| const resetAt = parseResetTime(data.limited_user_reset_date); |
|
|
| 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, |
| resetAt, |
| }, |
| completions: { |
| used: usedQuotas.completions || 0, |
| total: monthlyQuotas.completions || 0, |
| unlimited: false, |
| resetAt, |
| }, |
| }, |
| }; |
| } |
|
|
| 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 }; |
|
|
| return { |
| used: quota.entitlement - quota.remaining, |
| total: quota.entitlement, |
| remaining: quota.remaining, |
| unlimited: quota.unlimited || false, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| async function getGeminiUsage(accessToken, providerSpecificData, proxyOptions = null) { |
| if (!accessToken) { |
| return { plan: "Free", message: "Gemini CLI access token not available." }; |
| } |
|
|
| try { |
| |
| |
| 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 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; |
| 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}` }; |
| } |
| } |
|
|
| 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; |
| } |
|
|
| |
| |
| |
| async function getGeminiSubscriptionInfo(accessToken, proxyOptions = null) { |
| const controller = new AbortController(); |
| const timeoutId = setTimeout(() => controller.abort(), 10000); |
| try { |
| const response = await proxyAwareFetch( |
| "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", |
| { |
| method: "POST", |
| headers: { |
| Authorization: `Bearer ${accessToken}`, |
| "Content-Type": "application/json", |
| }, |
| body: JSON.stringify({ |
| metadata: CLIENT_METADATA, |
| }), |
| signal: controller.signal, |
| }, |
| proxyOptions |
| ); |
| if (!response.ok) return null; |
| return await response.json(); |
| } catch { |
| return null; |
| } finally { |
| clearTimeout(timeoutId); |
| } |
| } |
|
|
| |
| |
| |
| async function getAntigravityUsage(accessToken, providerSpecificData, proxyOptions = null) { |
| try { |
| |
| const subscriptionInfo = await getAntigravitySubscriptionInfo(accessToken, proxyOptions); |
| const projectId = subscriptionInfo?.cloudaicompanionProject || null; |
|
|
| |
| const controller = new AbortController(); |
| const timeoutId = setTimeout(() => controller.abort(), 10000); |
|
|
| let response; |
| try { |
| response = await proxyAwareFetch(ANTIGRAVITY_CONFIG.quotaApiUrl, { |
| 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", |
| }, |
| body: JSON.stringify({ |
| ...(projectId ? { project: projectId } : {}) |
| }), |
| signal: controller.signal, |
| }, proxyOptions); |
| } finally { |
| clearTimeout(timeoutId); |
| } |
|
|
| 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 = {}; |
|
|
| |
| if (data.models) { |
| |
| const importantModels = [ |
| 'gemini-3-flash-agent', |
| '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', |
| 'gemini-3-flash', |
| ]; |
|
|
| for (const [modelKey, info] of Object.entries(data.models)) { |
| |
| if (!info.quotaInfo) { |
| continue; |
| } |
|
|
| |
| if (info.isInternal || !importantModels.includes(modelKey)) { |
| continue; |
| } |
|
|
| const remainingFraction = info.quotaInfo.remainingFraction || 0; |
| const remainingPercentage = remainingFraction * 100; |
|
|
| |
| const total = 1000; |
| const remaining = Math.round(total * remainingFraction); |
| const used = total - remaining; |
|
|
| |
| quotas[modelKey] = { |
| used, |
| total, |
| resetAt: parseResetTime(info.quotaInfo.resetTime), |
| remainingPercentage, |
| unlimited: false, |
| displayName: info.displayName || modelKey, |
| }; |
| } |
| } |
|
|
| 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}` }; |
| } |
| } |
|
|
| |
| |
| |
| async function getAntigravityProjectId(accessToken) { |
| try { |
| const info = await getAntigravitySubscriptionInfo(accessToken); |
| return info?.cloudaicompanionProject || null; |
| } catch { |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| async function getAntigravitySubscriptionInfo(accessToken, proxyOptions = null) { |
| const controller = new AbortController(); |
| const timeoutId = setTimeout(() => controller.abort(), 10000); |
| try { |
| const response = await proxyAwareFetch(ANTIGRAVITY_CONFIG.loadProjectApiUrl, { |
| method: "POST", |
| headers: { |
| "Authorization": `Bearer ${accessToken}`, |
| "User-Agent": ANTIGRAVITY_CONFIG.userAgent, |
| "Content-Type": "application/json", |
| "x-request-source": "local", |
| }, |
| body: JSON.stringify({ metadata: CLIENT_METADATA, mode: 1 }), |
| signal: controller.signal, |
| }, proxyOptions); |
|
|
| if (!response.ok) return null; |
| return await response.json(); |
| } catch (error) { |
| console.error("[Antigravity Subscription] Error:", error.message); |
| return null; |
| } finally { |
| clearTimeout(timeoutId); |
| } |
| } |
|
|
| |
| |
| |
| async function getClaudeUsage(accessToken, proxyOptions = null) { |
| try { |
| |
| const oauthResponse = await proxyAwareFetch(CLAUDE_CONFIG.oauthUsageUrl, { |
| method: "GET", |
| headers: { |
| "Authorization": `Bearer ${accessToken}`, |
| "anthropic-beta": "oauth-2025-04-20", |
| "anthropic-version": CLAUDE_CONFIG.apiVersion, |
| }, |
| }, proxyOptions); |
|
|
| if (oauthResponse.ok) { |
| const data = await oauthResponse.json(); |
| const quotas = {}; |
|
|
| |
| 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); |
| } |
|
|
| |
| 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, |
| }; |
| } |
|
|
| |
| console.warn(`[Claude Usage] OAuth endpoint returned ${oauthResponse.status}, falling back to legacy`); |
| return await getClaudeUsageLegacy(accessToken, proxyOptions); |
| } catch (error) { |
| return { message: `Claude connected. Unable to fetch usage: ${error.message}` }; |
| } |
| } |
|
|
| |
| |
| |
| async function getClaudeUsageLegacy(accessToken, proxyOptions = null) { |
| try { |
| const settingsResponse = await proxyAwareFetch(CLAUDE_CONFIG.settingsUrl, { |
| method: "GET", |
| headers: { |
| "Authorization": `Bearer ${accessToken}`, |
| "anthropic-version": CLAUDE_CONFIG.apiVersion, |
| }, |
| }, proxyOptions); |
|
|
| if (settingsResponse.ok) { |
| const settings = await settingsResponse.json(); |
|
|
| if (settings.organization_id) { |
| const usageResponse = await proxyAwareFetch( |
| CLAUDE_CONFIG.usageUrl.replace("{org_id}", settings.organization_id), |
| { |
| method: "GET", |
| headers: { |
| "Authorization": `Bearer ${accessToken}`, |
| "anthropic-version": CLAUDE_CONFIG.apiVersion, |
| }, |
| }, |
| proxyOptions |
| ); |
|
|
| 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}` }; |
| } |
| } |
|
|
| |
| |
| |
| 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; |
| } |
|
|
| async function getCodexUsage(accessToken, proxyOptions = null) { |
| try { |
| const response = await proxyAwareFetch(CODEX_CONFIG.usageUrl, { |
| method: "GET", |
| headers: { |
| "Authorization": `Bearer ${accessToken}`, |
| "Accept": "application/json", |
| }, |
| }, proxyOptions); |
|
|
| if (!response.ok) { |
| return { message: `Codex connected. Usage API temporarily unavailable (${response.status}).` }; |
| } |
|
|
| const data = await response.json(); |
| const normalRateLimit = data.rate_limit || data.rate_limits || data.rate_limits_by_limit_id?.codex || {}; |
| const reviewRateLimit = getCodexReviewRateLimit(data); |
| const quotas = {}; |
|
|
| appendCodexQuotaWindows(quotas, "", normalRateLimit); |
| appendCodexQuotaWindows(quotas, "review", reviewRateLimit); |
|
|
| return { |
| plan: data.plan_type || data.summary?.plan || "unknown", |
| limitReached: getCodexRateLimitBody(normalRateLimit)?.limit_reached || false, |
| reviewLimitReached: getCodexRateLimitBody(reviewRateLimit)?.limit_reached || false, |
| quotas, |
| }; |
| } catch (error) { |
| throw new Error(`Failed to fetch Codex usage: ${error.message}`); |
| } |
| } |
|
|
| |
| |
| |
| 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, |
| }; |
|
|
| |
| 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, |
| }; |
| } |
|
|
| async function getKiroUsage(accessToken, providerSpecificData, proxyOptions = null) { |
| const authMethod = providerSpecificData?.authMethod || "builder-id"; |
| const profileArn = providerSpecificData?.profileArn || resolveDefaultProfileArn(authMethod); |
|
|
| const getUsageParams = new URLSearchParams({ |
| isEmailRequired: "true", |
| origin: "AI_EDITOR", |
| resourceType: "AGENTIC_REQUEST", |
| }); |
|
|
| |
| const attempts = [ |
| { |
| name: "codewhisperer-get", |
| run: async () => proxyAwareFetch( |
| `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", |
| }, |
| }, |
| proxyOptions |
| ), |
| }, |
| { |
| name: "codewhisperer-post", |
| run: async () => proxyAwareFetch("https://codewhisperer.us-east-1.amazonaws.com", { |
| method: "POST", |
| headers: { |
| "Authorization": `Bearer ${accessToken}`, |
| "Content-Type": "application/x-amz-json-1.0", |
| "x-amz-target": "AmazonCodeWhispererService.GetUsageLimits", |
| "Accept": "application/json", |
| }, |
| body: JSON.stringify({ |
| origin: "AI_EDITOR", |
| profileArn, |
| resourceType: "AGENTIC_REQUEST", |
| }), |
| }, proxyOptions), |
| }, |
| { |
| name: "q-get", |
| run: async () => { |
| const params = new URLSearchParams({ |
| origin: "AI_EDITOR", |
| profileArn, |
| resourceType: "AGENTIC_REQUEST", |
| }); |
| return proxyAwareFetch(`https://q.us-east-1.amazonaws.com/getUsageLimits?${params}`, { |
| method: "GET", |
| headers: { |
| "Authorization": `Bearer ${accessToken}`, |
| "Accept": "application/json", |
| }, |
| }, proxyOptions); |
| }, |
| }, |
| ]; |
|
|
| 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; |
| } |
|
|
| const data = await response.json(); |
| return parseKiroQuotaData(data); |
| } catch (error) { |
| errors.push(`${attempt.name}:${error.message}`); |
| } |
| } |
|
|
| 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: {}, |
| }; |
| } |
|
|
| |
| if (sawAuthError && (authMethod === "google" || authMethod === "github")) { |
| return { |
| message: "Kiro quota API authentication expired. Chat may still work.", |
| quotas: {}, |
| }; |
| } |
|
|
| 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: {}, |
| }; |
| } |
|
|
| |
| |
| |
| async function getQwenUsage(accessToken, providerSpecificData) { |
| try { |
| const resourceUrl = providerSpecificData?.resourceUrl; |
| if (!resourceUrl) { |
| return { message: "Qwen connected. No resource URL available." }; |
| } |
|
|
| |
| return { message: "Qwen connected. Usage tracked per request." }; |
| } catch (error) { |
| return { message: "Unable to fetch Qwen usage." }; |
| } |
| } |
|
|
| |
| |
| |
| async function getIflowUsage(accessToken) { |
| try { |
| |
| return { message: "iFlow connected. Usage tracked per request." }; |
| } catch (error) { |
| return { message: "Unable to fetch iFlow usage." }; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function getOllamaUsage(accessToken, providerSpecificData) { |
| try { |
| |
| |
| |
| 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." }; |
| } |
| } |
|
|
| |
| |
| |
| 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}` }; |
| } |
| } |
|
|
| |
| function getMiniMaxField(model, snakeKey, camelKey) { |
| if (!model || typeof model !== "object") return null; |
| return model[snakeKey] ?? model[camelKey] ?? null; |
| } |
|
|
| function getMiniMaxModelName(model) { |
| return String(getMiniMaxField(model, "model_name", "modelName") || "").trim(); |
| } |
|
|
| function formatMiniMaxQuotaName(model) { |
| const rawName = getMiniMaxModelName(model); |
| if (!rawName) return "MiniMax"; |
|
|
| |
| |
| |
| |
| if (rawName === "MiniMax-M*" || rawName === "general") return "M-series"; |
|
|
| 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"); |
| } |
|
|
| function getMiniMaxProvidedPercent(model, snakeKey, camelKey) { |
| if (!model || typeof model !== "object") return null; |
| const raw = model[snakeKey] ?? model[camelKey]; |
| if (raw === null || raw === undefined) return null; |
| const num = Number(raw); |
| if (!Number.isFinite(num)) return null; |
| return Math.max(0, Math.min(100, num)); |
| } |
|
|
| 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); |
| } |
|
|
| function hasMiniMaxQuota(model) { |
| |
| |
| if (getMiniMaxSessionTotal(model) > 0 || getMiniMaxWeeklyTotal(model) > 0) return true; |
| if (getMiniMaxProvidedPercent(model, "current_interval_remaining_percent", "currentIntervalRemainingPercent") !== null) return true; |
| if (getMiniMaxProvidedPercent(model, "current_weekly_remaining_percent", "currentWeeklyRemainingPercent") !== null) return true; |
| return false; |
| } |
|
|
| 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, providedPercent = null) { |
| 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); |
| |
| |
| |
| |
| const remainingPercentage = providedPercentage(providedPercent, remaining, safeTotal); |
| return { |
| used, |
| total: safeTotal, |
| remaining, |
| remainingPercentage, |
| resetAt, |
| unlimited: false, |
| }; |
| } |
|
|
| function providedPercentage(provided, remaining, total) { |
| if (provided !== null && provided !== undefined && Number.isFinite(provided)) { |
| return Math.max(0, Math.min(100, provided)); |
| } |
| return total > 0 ? Math.max(0, Math.min(100, (remaining / total) * 100)) : 0; |
| } |
|
|
| function addMiniMaxQuota(quotas, key, model, getTotal, countSnake, countCamel, percentSnake, percentCamel, resetArgs, countMeansRemaining) { |
| const total = getTotal(model); |
| const providedPercent = getMiniMaxProvidedPercent(model, percentSnake, percentCamel); |
| if (total <= 0 && providedPercent === null) return; |
|
|
| const count = Math.max(0, Number(getMiniMaxField(model, countSnake, countCamel)) || 0); |
| let effectiveTotal = total; |
| let effectiveCount = count; |
| if (total <= 0) { |
| |
| |
| |
| |
| effectiveTotal = 100; |
| const pct = providedPercent; |
| effectiveCount = countMeansRemaining |
| ? Math.round(effectiveTotal * (pct / 100)) |
| : Math.round(effectiveTotal * (1 - pct / 100)); |
| } |
| quotas[key] = buildMiniMaxQuota( |
| effectiveTotal, |
| effectiveCount, |
| getMiniMaxResetAt(model, ...resetArgs), |
| countMeansRemaining, |
| providedPercent |
| ); |
| } |
|
|
| |
| |
| |
| 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 : []; |
| const quotaModels = allModels.filter(hasMiniMaxQuota); |
|
|
| if (quotaModels.length === 0) { |
| return { message: "MiniMax connected. No quota data was returned." }; |
| } |
|
|
| const capturedAtMs = Date.now(); |
| const countMeansRemaining = usageUrl.includes("/coding_plan/remains"); |
| const quotas = {}; |
|
|
| for (const model of quotaModels) { |
| const displayName = formatMiniMaxQuotaName(model); |
| addMiniMaxQuota( |
| quotas, |
| `${displayName} (5h)`, |
| model, |
| getMiniMaxSessionTotal, |
| "current_interval_usage_count", |
| "currentIntervalUsageCount", |
| "current_interval_remaining_percent", |
| "currentIntervalRemainingPercent", |
| [capturedAtMs, "remains_time", "remainsTime", "end_time", "endTime"], |
| countMeansRemaining |
| ); |
|
|
| addMiniMaxQuota( |
| quotas, |
| `${displayName} (7d)`, |
| model, |
| getMiniMaxWeeklyTotal, |
| "current_weekly_usage_count", |
| "currentWeeklyUsageCount", |
| "current_weekly_remaining_percent", |
| "currentWeeklyRemainingPercent", |
| [capturedAtMs, "weekly_remains_time", "weeklyRemainsTime", "weekly_end_time", "weeklyEndTime"], |
| 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." }; |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function getVercelAiGatewayUsage(apiKey, proxyOptions = null) { |
| if (!apiKey) { |
| return { message: "Vercel AI Gateway API key not available." }; |
| } |
|
|
| try { |
| const response = await proxyAwareFetch(VERCEL_AI_GATEWAY_CREDITS_URL, { |
| method: "GET", |
| headers: { |
| Authorization: `Bearer ${apiKey}`, |
| Accept: "application/json", |
| }, |
| }, proxyOptions); |
|
|
| if (response.status === 401 || response.status === 403) { |
| return { message: "Vercel AI Gateway API key invalid or expired." }; |
| } |
|
|
| if (!response.ok) { |
| const errorText = await response.text().catch(() => ""); |
| const trimmed = errorText ? `: ${errorText.slice(0, 200)}` : ""; |
| return { message: `Vercel AI Gateway credits API error (${response.status})${trimmed}` }; |
| } |
|
|
| const data = await response.json(); |
|
|
| |
| const balance = Number(data?.balance) || 0; |
| const totalUsed = Number(data?.total_used) || 0; |
|
|
| |
| |
| const MONTHLY_CREDIT = 5; |
| const remainingPercentage = (balance / MONTHLY_CREDIT) * 100; |
|
|
| if (balance <= 0 && totalUsed <= 0) { |
| return { |
| plan: "Pay-as-you-go", |
| message: "Vercel AI Gateway connected. No credit allocation found (BYOK or unfunded account).", |
| quotas: {}, |
| }; |
| } |
|
|
| |
| |
| return { |
| plan: "Pay-as-you-go", |
| quotas: { |
| "Used (USD)": { |
| used: totalUsed, |
| total: 0, |
| remaining: 0, |
| remainingPercentage: 100, |
| unlimited: true, |
| }, |
| "Remaining (USD)": { |
| used: balance, |
| total: MONTHLY_CREDIT, |
| remaining: balance, |
| remainingPercentage, |
| unlimited: false, |
| }, |
| }, |
| }; |
| } catch (error) { |
| return { message: `Vercel AI Gateway error: ${error.message}` }; |
| } |
| } |
|
|
| 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." }; |
| } |
| |
| |
| |
| const userQuota = body.userQuota || {}; |
| const orgQuota = body.orgResourcePackage || {}; |
| |
| |
| |
| const expiresAtMs = Number.isFinite(Number(body.expiresAt)) && Number(body.expiresAt) > 0 |
| ? Number(body.expiresAt) |
| : null; |
| const resetAt = expiresAtMs ? new Date(expiresAtMs).toISOString() : null; |
| const quotas = { |
| user: { |
| total: Number(userQuota.total) || 0, |
| used: Number(userQuota.used) || 0, |
| remaining: Number(userQuota.remaining) || 0, |
| unit: userQuota.unit || "credits", |
| resetAt, |
| }, |
| organization: { |
| total: Number(orgQuota.total) || 0, |
| used: Number(orgQuota.used) || 0, |
| remaining: Number(orgQuota.remaining) || 0, |
| unit: orgQuota.unit || "credits", |
| resetAt, |
| }, |
| }; |
| return { |
| quotas, |
| totalUsagePercentage: Number(body.totalUsagePercentage) || 0, |
| isQuotaExceeded: !!body.isQuotaExceeded, |
| expiresAt: expiresAtMs, |
| }; |
| } catch (error) { |
| return { message: `Qoder connected. Unable to fetch usage: ${error.message}` }; |
| } |
| } |
|
|