Spaces:
Running
Running
| /** | |
| * Rate Limit Management | |
| * | |
| * Handles rate limit tracking and state management for accounts. | |
| * All rate limits are model-specific. | |
| */ | |
| import { DEFAULT_COOLDOWN_MS } from '../constants.js'; | |
| import { formatDuration } from '../utils/helpers.js'; | |
| import { logger } from '../utils/logger.js'; | |
| /** | |
| * Check if all accounts are rate-limited for a specific model | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @param {string} modelId - Model ID to check rate limits for | |
| * @returns {boolean} True if all accounts are rate-limited | |
| */ | |
| export function isAllRateLimited(accounts, modelId) { | |
| if (accounts.length === 0) return true; | |
| if (!modelId) return false; // No model specified = not rate limited | |
| return accounts.every(acc => { | |
| if (acc.isInvalid) return true; // Invalid accounts count as unavailable | |
| if (acc.enabled === false) return true; // Disabled accounts count as unavailable | |
| const modelLimits = acc.modelRateLimits || {}; | |
| const limit = modelLimits[modelId]; | |
| return limit && limit.isRateLimited && limit.resetTime > Date.now(); | |
| }); | |
| } | |
| /** | |
| * Get list of available (non-rate-limited, non-invalid) accounts for a model | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @param {string} [modelId] - Model ID to filter by | |
| * @returns {Array} Array of available account objects | |
| */ | |
| export function getAvailableAccounts(accounts, modelId = null) { | |
| return accounts.filter(acc => { | |
| if (acc.isInvalid) return false; | |
| // WebUI: Skip disabled accounts | |
| if (acc.enabled === false) return false; | |
| if (modelId && acc.modelRateLimits && acc.modelRateLimits[modelId]) { | |
| const limit = acc.modelRateLimits[modelId]; | |
| if (limit.isRateLimited && limit.resetTime > Date.now()) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| }); | |
| } | |
| /** | |
| * Get list of invalid accounts | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @returns {Array} Array of invalid account objects | |
| */ | |
| export function getInvalidAccounts(accounts) { | |
| return accounts.filter(acc => acc.isInvalid); | |
| } | |
| /** | |
| * Clear expired rate limits | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @returns {number} Number of rate limits cleared | |
| */ | |
| export function clearExpiredLimits(accounts) { | |
| const now = Date.now(); | |
| let cleared = 0; | |
| for (const account of accounts) { | |
| if (account.modelRateLimits) { | |
| for (const [modelId, limit] of Object.entries(account.modelRateLimits)) { | |
| if (limit.isRateLimited && limit.resetTime <= now) { | |
| limit.isRateLimited = false; | |
| limit.resetTime = null; | |
| cleared++; | |
| logger.success(`[AccountManager] Rate limit expired for: ${account.email} (model: ${modelId})`); | |
| } | |
| } | |
| } | |
| } | |
| return cleared; | |
| } | |
| /** | |
| * Clear all rate limits to force a fresh check (optimistic retry strategy) | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| */ | |
| export function resetAllRateLimits(accounts) { | |
| for (const account of accounts) { | |
| if (account.modelRateLimits) { | |
| for (const key of Object.keys(account.modelRateLimits)) { | |
| account.modelRateLimits[key] = { isRateLimited: false, resetTime: null }; | |
| } | |
| } | |
| } | |
| logger.warn('[AccountManager] Reset all rate limits for optimistic retry'); | |
| } | |
| /** | |
| * Mark an account as rate-limited for a specific model | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @param {string} email - Email of the account to mark | |
| * @param {number|null} resetMs - Time in ms until rate limit resets (from API) | |
| * @param {string} modelId - Model ID to mark rate limit for | |
| * @returns {boolean} True if account was found and marked | |
| */ | |
| export function markRateLimited(accounts, email, resetMs = null, modelId) { | |
| const account = accounts.find(a => a.email === email); | |
| if (!account) return false; | |
| // Store the ACTUAL reset time from the API | |
| // This is used to decide whether to wait (short) or switch accounts (long) | |
| const actualResetMs = (resetMs && resetMs > 0) ? resetMs : DEFAULT_COOLDOWN_MS; | |
| if (!account.modelRateLimits) { | |
| account.modelRateLimits = {}; | |
| } | |
| account.modelRateLimits[modelId] = { | |
| isRateLimited: true, | |
| resetTime: Date.now() + actualResetMs, // Actual reset time for decisions | |
| actualResetMs: actualResetMs // Original duration from API | |
| }; | |
| // Track consecutive failures for progressive backoff (matches opencode-antigravity-auth) | |
| account.consecutiveFailures = (account.consecutiveFailures || 0) + 1; | |
| // Log appropriately based on duration | |
| if (actualResetMs > DEFAULT_COOLDOWN_MS) { | |
| logger.warn( | |
| `[AccountManager] Quota exhausted: ${email} (model: ${modelId}). Resets in ${formatDuration(actualResetMs)}` | |
| ); | |
| } else { | |
| logger.warn( | |
| `[AccountManager] Rate limited: ${email} (model: ${modelId}). Available in ${formatDuration(actualResetMs)}` | |
| ); | |
| } | |
| return true; | |
| } | |
| /** | |
| * Mark an account as invalid (credentials need re-authentication) | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @param {string} email - Email of the account to mark | |
| * @param {string} reason - Reason for marking as invalid | |
| * @param {string|null} verifyUrl - Optional verification URL (for 403 VALIDATION_REQUIRED) | |
| * @returns {boolean} True if account was found and marked | |
| */ | |
| export function markInvalid(accounts, email, reason = 'Unknown error', verifyUrl = null) { | |
| const account = accounts.find(a => a.email === email); | |
| if (!account) return false; | |
| account.isInvalid = true; | |
| account.invalidReason = reason; | |
| account.invalidAt = Date.now(); | |
| account.verifyUrl = verifyUrl || null; | |
| logger.error( | |
| `[AccountManager] ⚠ Account INVALID: ${email}` | |
| ); | |
| logger.error( | |
| `[AccountManager] Reason: ${reason}` | |
| ); | |
| if (verifyUrl) { | |
| logger.error( | |
| `[AccountManager] Verification URL: ${verifyUrl}` | |
| ); | |
| } | |
| logger.error( | |
| `[AccountManager] Run 'npm run accounts' to re-authenticate this account` | |
| ); | |
| return true; | |
| } | |
| /** | |
| * Clear invalid status for an account (after user completes verification) | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @param {string} email - Email of the account to clear | |
| * @returns {boolean} True if account was found and cleared | |
| */ | |
| export function clearInvalid(accounts, email) { | |
| const account = accounts.find(a => a.email === email); | |
| if (!account) return false; | |
| account.isInvalid = false; | |
| account.invalidReason = null; | |
| account.verifyUrl = null; | |
| logger.info(`[AccountManager] ✓ Account re-enabled: ${email}`); | |
| return true; | |
| } | |
| /** | |
| * Get the minimum wait time until any account becomes available for a model | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @param {string} modelId - Model ID to check | |
| * @returns {number} Wait time in milliseconds | |
| */ | |
| export function getMinWaitTimeMs(accounts, modelId) { | |
| if (!isAllRateLimited(accounts, modelId)) return 0; | |
| const now = Date.now(); | |
| let minWait = Infinity; | |
| let soonestAccount = null; | |
| for (const account of accounts) { | |
| if (modelId && account.modelRateLimits && account.modelRateLimits[modelId]) { | |
| const limit = account.modelRateLimits[modelId]; | |
| if (limit.isRateLimited && limit.resetTime) { | |
| const wait = limit.resetTime - now; | |
| if (wait > 0 && wait < minWait) { | |
| minWait = wait; | |
| soonestAccount = account; | |
| } | |
| } | |
| } | |
| } | |
| if (soonestAccount) { | |
| logger.info(`[AccountManager] Shortest wait: ${formatDuration(minWait)} (account: ${soonestAccount.email})`); | |
| } | |
| return minWait === Infinity ? DEFAULT_COOLDOWN_MS : minWait; | |
| } | |
| /** | |
| * Get the rate limit info for a specific account and model | |
| * Returns the actual reset time from API, not capped | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @param {string} email - Email of the account | |
| * @param {string} modelId - Model ID to check | |
| * @returns {{isRateLimited: boolean, actualResetMs: number|null, waitMs: number}} Rate limit info | |
| */ | |
| export function getRateLimitInfo(accounts, email, modelId) { | |
| const account = accounts.find(a => a.email === email); | |
| if (!account || !account.modelRateLimits || !account.modelRateLimits[modelId]) { | |
| return { isRateLimited: false, actualResetMs: null, waitMs: 0 }; | |
| } | |
| const limit = account.modelRateLimits[modelId]; | |
| const now = Date.now(); | |
| const waitMs = limit.resetTime ? Math.max(0, limit.resetTime - now) : 0; | |
| return { | |
| isRateLimited: limit.isRateLimited && waitMs > 0, | |
| actualResetMs: limit.actualResetMs || null, | |
| waitMs | |
| }; | |
| } | |
| /** | |
| * Get the consecutive failure count for an account | |
| * Used for progressive backoff calculation (matches opencode-antigravity-auth) | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @param {string} email - Email of the account | |
| * @returns {number} Number of consecutive failures | |
| */ | |
| export function getConsecutiveFailures(accounts, email) { | |
| const account = accounts.find(a => a.email === email); | |
| return account?.consecutiveFailures || 0; | |
| } | |
| /** | |
| * Reset the consecutive failure count for an account | |
| * Called on successful request (matches opencode-antigravity-auth) | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @param {string} email - Email of the account | |
| * @returns {boolean} True if account was found and reset | |
| */ | |
| export function resetConsecutiveFailures(accounts, email) { | |
| const account = accounts.find(a => a.email === email); | |
| if (!account) return false; | |
| account.consecutiveFailures = 0; | |
| return true; | |
| } | |
| /** | |
| * Increment the consecutive failure count for an account WITHOUT marking as rate limited | |
| * Used for quick retries where we want to track failures but not skip the account | |
| * (matches opencode-antigravity-auth behavior of always incrementing on 429) | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @param {string} email - Email of the account | |
| * @returns {number} New consecutive failure count | |
| */ | |
| export function incrementConsecutiveFailures(accounts, email) { | |
| const account = accounts.find(a => a.email === email); | |
| if (!account) return 0; | |
| account.consecutiveFailures = (account.consecutiveFailures || 0) + 1; | |
| return account.consecutiveFailures; | |
| } | |
| // ============================================================================ | |
| // Cooldown Mechanism (matches opencode-antigravity-auth) | |
| // Separate from rate limits - used for temporary backoff after failures | |
| // ============================================================================ | |
| /** | |
| * Cooldown reasons for debugging/logging | |
| */ | |
| export const CooldownReason = { | |
| RATE_LIMIT: 'rate_limit', | |
| AUTH_FAILURE: 'auth_failure', | |
| CONSECUTIVE_FAILURES: 'consecutive_failures', | |
| SERVER_ERROR: 'server_error' | |
| }; | |
| /** | |
| * Mark an account as cooling down for a specified duration | |
| * Used for temporary backoff separate from rate limits | |
| * | |
| * @param {Array} accounts - Array of account objects | |
| * @param {string} email - Email of the account | |
| * @param {number} cooldownMs - Duration of cooldown in milliseconds | |
| * @param {string} [reason] - Reason for the cooldown | |
| * @returns {boolean} True if account was found and marked | |
| */ | |
| export function markAccountCoolingDown(accounts, email, cooldownMs, reason = CooldownReason.RATE_LIMIT) { | |
| const account = accounts.find(a => a.email === email); | |
| if (!account) return false; | |
| account.coolingDownUntil = Date.now() + cooldownMs; | |
| account.cooldownReason = reason; | |
| logger.debug(`[AccountManager] Account ${email} cooling down for ${formatDuration(cooldownMs)} (reason: ${reason})`); | |
| return true; | |
| } | |
| /** | |
| * Check if an account is currently cooling down | |
| * Automatically clears expired cooldowns | |
| * | |
| * @param {Object} account - Account object | |
| * @returns {boolean} True if account is cooling down | |
| */ | |
| export function isAccountCoolingDown(account) { | |
| if (!account || account.coolingDownUntil === undefined) { | |
| return false; | |
| } | |
| const now = Date.now(); | |
| if (now >= account.coolingDownUntil) { | |
| // Cooldown expired - clear it | |
| clearAccountCooldown(account); | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * Clear the cooldown for an account | |
| * | |
| * @param {Object} account - Account object | |
| */ | |
| export function clearAccountCooldown(account) { | |
| if (account) { | |
| delete account.coolingDownUntil; | |
| delete account.cooldownReason; | |
| } | |
| } | |
| /** | |
| * Get time remaining until cooldown expires for an account | |
| * | |
| * @param {Object} account - Account object | |
| * @returns {number} Milliseconds until cooldown expires, 0 if not cooling down | |
| */ | |
| export function getCooldownRemaining(account) { | |
| if (!account || account.coolingDownUntil === undefined) { | |
| return 0; | |
| } | |
| const remaining = account.coolingDownUntil - Date.now(); | |
| return remaining > 0 ? remaining : 0; | |
| } | |