| import { getModelsByProviderId } from "open-sse/config/providerModels.js"; |
|
|
| |
| export const QUOTA_CACHE_KEY = "quotaCacheData"; |
| export const REFRESH_INTERVAL_MS = 60000; |
| |
| export const CLAUDE_REFRESH_INTERVAL_MS = 180000; |
| export const DEPLETED_QUOTA_THRESHOLD = 5; |
| export const AUTO_REFRESH_STORAGE_KEY = "quotaAutoRefresh"; |
| export const CONNECTIONS_PAGE_SIZE = 20; |
| export const ACCOUNT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; |
| export const ACCOUNT_PAGE_SIZE_MAX = 500; |
| export const ACCOUNT_FILTER_OPTIONS = [ |
| { value: "all", label: "All accounts" }, |
| { value: "active", label: "Active" }, |
| { value: "inactive", label: "Turned off" }, |
| ]; |
| export const QUOTA_SORT_OPTIONS = [ |
| { value: "default", label: "Default quota order" }, |
| { value: "remaining-asc", label: "% quota: low to high" }, |
| { value: "remaining-desc", label: "% quota: high to low" }, |
| ]; |
|
|
| |
| export function getConnectionLabel(connection) { |
| return connection.name?.trim() |
| || connection.email?.trim() |
| || connection.displayName?.trim() |
| || null; |
| } |
|
|
| export function getConnectionQuotaRemaining(connection, quotaData) { |
| const quota = quotaData[connection.id]?.quotas?.[0]; |
| if (!quota) return Number.POSITIVE_INFINITY; |
| if (typeof quota.remaining === "number") return quota.remaining; |
| return Number.POSITIVE_INFINITY; |
| } |
|
|
| export function sortVisibleConnections( |
| connections, |
| quotaData, |
| expiringFirst, |
| providerFilter, |
| quotaSortMode, |
| ) { |
| if (providerFilter === "codex" && quotaSortMode !== "default") { |
| return [...connections].sort((a, b) => { |
| const remainingA = getConnectionQuotaRemaining(a, quotaData); |
| const remainingB = getConnectionQuotaRemaining(b, quotaData); |
| const remainingDiff = |
| quotaSortMode === "remaining-asc" |
| ? remainingA - remainingB |
| : remainingB - remainingA; |
| if (remainingDiff !== 0) return remainingDiff; |
| return (getConnectionLabel(a) || "").localeCompare( |
| getConnectionLabel(b) || "", |
| ); |
| }); |
| } |
|
|
| if (!expiringFirst) return connections; |
|
|
| const getEarliestResetTime = (connection) => { |
| const resetTimes = (quotaData[connection.id]?.quotas || []) |
| .map((quota) => |
| quota.resetAt |
| ? new Date(quota.resetAt).getTime() |
| : Number.POSITIVE_INFINITY, |
| ) |
| .filter((time) => Number.isFinite(time)); |
| return resetTimes.length > 0 |
| ? Math.min(...resetTimes) |
| : Number.POSITIVE_INFINITY; |
| }; |
|
|
| return [...connections].sort((a, b) => { |
| const expiryDiff = getEarliestResetTime(a) - getEarliestResetTime(b); |
| if (expiryDiff !== 0) return expiryDiff; |
| return ( |
| (a.provider || "").localeCompare(b.provider || "") || |
| (getConnectionLabel(a) || "").localeCompare(getConnectionLabel(b) || "") |
| ); |
| }); |
| } |
|
|
| export function buildLoadingState(connections) { |
| const nextLoadingState = {}; |
| connections.forEach((connection) => { |
| nextLoadingState[connection.id] = true; |
| }); |
| return nextLoadingState; |
| } |
|
|
| export function filterQuotaStateByConnections(state, connections) { |
| const visibleIds = new Set(connections.map((connection) => connection.id)); |
| return Object.fromEntries( |
| Object.entries(state).filter(([id]) => visibleIds.has(id)), |
| ); |
| } |
|
|
| export function getConnectionsPageRange(pagination) { |
| if (!pagination.total) { |
| return { start: 0, end: 0 }; |
| } |
| const start = (pagination.page - 1) * pagination.pageSize + 1; |
| const end = Math.min(pagination.page * pagination.pageSize, pagination.total); |
| return { start, end }; |
| } |
|
|
| export function getConnectionsEmptyMessage(totals, providerFilter, accountFilter) { |
| if (!totals.eligibleConnections) { |
| return { |
| icon: "cloud_off", |
| title: "No Providers Connected", |
| description: |
| "Connect to providers with OAuth to track your API quota limits and usage.", |
| }; |
| } |
| if (!totals.providerFilteredConnections) { |
| return { |
| icon: "filter_alt_off", |
| title: "No Accounts Match Current Filters", |
| description: |
| providerFilter === "all" |
| ? "Try changing the account status filter to see more quota trackers." |
| : `No ${accountFilter === "inactive" ? "turned off" : accountFilter === "active" ? "active" : "matching"} accounts found for ${providerFilter}.`, |
| }; |
| } |
| return { |
| icon: "filter_alt_off", |
| title: "No Accounts On This Page", |
| description: |
| "Try moving to another page or refreshing the current filters.", |
| }; |
| } |
|
|
| export function sortRequestFromExpiringFirst(expiringFirst) { |
| return expiringFirst ? "expiring" : "priority"; |
| } |
|
|
| export function getPageSizeLabel(pageSize, isCustomPageSize) { |
| return isCustomPageSize ? `Custom: ${pageSize} / page` : `${pageSize} / page`; |
| } |
|
|
| export function getConnectionsPaginationSummary(pagination) { |
| const { start, end } = getConnectionsPageRange(pagination); |
| return `Showing ${start}-${end} of ${pagination.total}`; |
| } |
|
|
| export function getSafePagination(pagination, fallbackPageSize) { |
| return ( |
| pagination || { |
| page: 1, |
| pageSize: fallbackPageSize, |
| total: 0, |
| totalPages: 1, |
| } |
| ); |
| } |
|
|
| export function getSafeTotals(totals, fallbackTotal = 0) { |
| return ( |
| totals || { |
| eligibleConnections: fallbackTotal, |
| providerFilteredConnections: fallbackTotal, |
| } |
| ); |
| } |
|
|
| export function shouldResetPage(previousValue, nextValue) { |
| return previousValue !== nextValue; |
| } |
|
|
| export function getPaginationPageValue(dataPagination, fallbackPage) { |
| return dataPagination?.page || fallbackPage; |
| } |
|
|
| export function getProviderOptions(dataProviderOptions) { |
| return dataProviderOptions || []; |
| } |
|
|
| export async function reconcileConnectionsPage(fetchConnections, targetPage) { |
| return await fetchConnections(targetPage); |
| } |
|
|
| export function getQuotaCache() { |
| if (typeof window === "undefined") return {}; |
| try { |
| const cached = window.localStorage.getItem(QUOTA_CACHE_KEY); |
| return cached ? JSON.parse(cached) : {}; |
| } catch (error) { |
| console.error("Error reading quota cache:", error); |
| return {}; |
| } |
| } |
|
|
| export function setQuotaCache(connectionId, quotaEntry) { |
| if (typeof window === "undefined") return; |
| try { |
| const cache = getQuotaCache(); |
| cache[connectionId] = { |
| ...quotaEntry, |
| cachedAt: new Date().toISOString(), |
| }; |
| window.localStorage.setItem(QUOTA_CACHE_KEY, JSON.stringify(cache)); |
| } catch (error) { |
| console.error("Error writing quota cache:", error); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function formatResetTime(date) { |
| if (!date) return "-"; |
|
|
| try { |
| const resetDate = typeof date === "string" ? new Date(date) : date; |
| const now = new Date(); |
| const diffMs = resetDate - now; |
|
|
| if (diffMs <= 0) return "-"; |
|
|
| const totalMinutes = Math.ceil(diffMs / (1000 * 60)); |
| |
| |
| if (totalMinutes < 60) { |
| return `${totalMinutes}m`; |
| } |
| |
| const totalHours = Math.floor(totalMinutes / 60); |
| const remainingMinutes = totalMinutes % 60; |
| |
| |
| if (totalHours < 24) { |
| return `${totalHours}h ${remainingMinutes}m`; |
| } |
| |
| |
| const days = Math.floor(totalHours / 24); |
| const remainingHours = totalHours % 24; |
| return `${days}d ${remainingHours}h ${remainingMinutes}m`; |
| } catch (error) { |
| return "-"; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function getStatusColor(percentage) { |
| if (percentage > 70) return "green"; |
| if (percentage >= 30) return "yellow"; |
| return "red"; |
| } |
|
|
| |
| |
| |
| |
| |
| export function getStatusEmoji(percentage) { |
| if (percentage > 70) return "🟢"; |
| if (percentage >= 30) return "🟡"; |
| return "🔴"; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function calculatePercentage(used, total) { |
| if (!total || total === 0) return 0; |
| if (!used || used < 0) return 100; |
| if (used >= total) return 0; |
|
|
| return Math.round(((total - used) / total) * 100); |
| } |
|
|
| |
| |
| |
| |
| |
| export function getRemainingPercentage(quota) { |
| if (quota?.remaining !== undefined) { |
| return Math.max(0, Math.round(quota.remaining)); |
| } |
|
|
| if (quota?.remainingPercentage !== undefined) { |
| return Math.round(quota.remainingPercentage); |
| } |
|
|
| return calculatePercentage(quota?.used, quota?.total); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function parseQuotaData(provider, data) { |
| if (!data || typeof data !== "object") return []; |
|
|
| const normalizedQuotas = []; |
|
|
| try { |
| switch (provider.toLowerCase()) { |
| case "github": |
| if (data.quotas) { |
| Object.entries(data.quotas).forEach(([name, quota]) => { |
| normalizedQuotas.push({ |
| name, |
| used: quota.used || 0, |
| total: quota.total || 0, |
| resetAt: quota.resetAt || null, |
| }); |
| }); |
| } |
| break; |
|
|
| case "antigravity": |
| if (data.quotas) { |
| Object.entries(data.quotas).forEach(([modelKey, quota]) => { |
| normalizedQuotas.push({ |
| name: quota.displayName || modelKey, |
| modelKey: modelKey, |
| used: quota.used || 0, |
| total: quota.total || 0, |
| resetAt: quota.resetAt || null, |
| remainingPercentage: quota.remainingPercentage, |
| }); |
| }); |
| } |
| break; |
|
|
| case "codex": |
| if (data.quotas) { |
| Object.entries(data.quotas).forEach(([quotaType, quota]) => { |
| normalizedQuotas.push({ |
| name: quotaType, |
| used: quota.used || 0, |
| total: quota.total || 0, |
| remaining: quota.remaining, |
| resetAt: quota.resetAt || null, |
| }); |
| }); |
| } |
| break; |
|
|
| case "kiro": |
| if (data.quotas) { |
| Object.entries(data.quotas).forEach(([quotaType, quota]) => { |
| normalizedQuotas.push({ |
| name: quotaType, |
| used: quota.used || 0, |
| total: quota.total || 0, |
| resetAt: quota.resetAt || null, |
| }); |
| }); |
| } |
| break; |
|
|
| case "qoder": |
| |
| |
| |
| |
| |
| |
| |
| |
| if (data.quotas) { |
| Object.entries(data.quotas).forEach(([quotaType, quota]) => { |
| if (quotaType === "organization" && (!quota || (Number(quota.total) || 0) === 0)) { |
| return; |
| } |
| normalizedQuotas.push({ |
| name: quotaType === "user" ? "Personal" : quotaType === "organization" ? "Organization" : quotaType, |
| used: quota.used || 0, |
| total: quota.total || 0, |
| unit: quota.unit, |
| resetAt: quota.resetAt || null, |
| }); |
| }); |
| } |
| break; |
|
|
| case "claude": |
| if (data.message) { |
| |
| normalizedQuotas.push({ |
| name: "error", |
| used: 0, |
| total: 0, |
| resetAt: null, |
| message: data.message, |
| }); |
| } else if (data.quotas) { |
| Object.entries(data.quotas).forEach(([name, quota]) => { |
| normalizedQuotas.push({ |
| name, |
| used: quota.used || 0, |
| total: quota.total || 0, |
| resetAt: quota.resetAt || null, |
| }); |
| }); |
| } |
| break; |
|
|
| case "vercel-ai-gateway": |
| |
| |
| |
| |
| if (data.quotas) { |
| Object.entries(data.quotas).forEach(([name, quota]) => { |
| normalizedQuotas.push({ |
| name, |
| used: quota.used || 0, |
| total: quota.total || 0, |
| resetAt: quota.resetAt || null, |
| remainingPercentage: quota.remainingPercentage, |
| }); |
| }); |
| } |
| break; |
|
|
| default: |
| |
| if (data.quotas) { |
| Object.entries(data.quotas).forEach(([name, quota]) => { |
| normalizedQuotas.push({ |
| name, |
| used: quota.used || 0, |
| total: quota.total || 0, |
| resetAt: quota.resetAt || null, |
| }); |
| }); |
| } |
| } |
| } catch (error) { |
| console.error(`Error parsing quota data for ${provider}:`, error); |
| return []; |
| } |
|
|
| |
| const modelOrder = getModelsByProviderId(provider); |
| if (modelOrder.length > 0) { |
| const orderMap = new Map(modelOrder.map((m, i) => [m.id, i])); |
| |
| normalizedQuotas.sort((a, b) => { |
| |
| const keyA = a.modelKey || a.name; |
| const keyB = b.modelKey || b.name; |
| const orderA = orderMap.get(keyA) ?? 999; |
| const orderB = orderMap.get(keyB) ?? 999; |
| return orderA - orderB; |
| }); |
| } |
|
|
| return normalizedQuotas; |
| } |
|
|