/** * Custom Error Classes * * Provides structured error types for better error handling and classification. * Replaces string-based error detection with proper error class checking. */ /** * Base error class for Antigravity proxy errors */ export class AntigravityError extends Error { /** * @param {string} message - Error message * @param {string} code - Error code for programmatic handling * @param {boolean} retryable - Whether the error is retryable * @param {Object} metadata - Additional error metadata */ constructor(message, code, retryable = false, metadata = {}) { super(message); this.name = 'AntigravityError'; this.code = code; this.retryable = retryable; this.metadata = metadata; } /** * Convert to JSON for API responses */ toJSON() { return { name: this.name, code: this.code, message: this.message, retryable: this.retryable, ...this.metadata }; } } /** * Rate limit error (429 / RESOURCE_EXHAUSTED) */ export class RateLimitError extends AntigravityError { /** * @param {string} message - Error message * @param {number|null} resetMs - Time in ms until rate limit resets * @param {string} accountEmail - Email of the rate-limited account */ constructor(message, resetMs = null, accountEmail = null) { super(message, 'RATE_LIMITED', true, { resetMs, accountEmail }); this.name = 'RateLimitError'; this.resetMs = resetMs; this.accountEmail = accountEmail; } } /** * Authentication error (invalid credentials, token expired, etc.) */ export class AuthError extends AntigravityError { /** * @param {string} message - Error message * @param {string} accountEmail - Email of the account with auth issues * @param {string} reason - Specific reason for auth failure */ constructor(message, accountEmail = null, reason = null) { super(message, 'AUTH_INVALID', false, { accountEmail, reason }); this.name = 'AuthError'; this.accountEmail = accountEmail; this.reason = reason; } } /** * No accounts available error */ export class NoAccountsError extends AntigravityError { /** * @param {string} message - Error message * @param {boolean} allRateLimited - Whether all accounts are rate limited */ constructor(message = 'No accounts available', allRateLimited = false) { super(message, 'NO_ACCOUNTS', allRateLimited, { allRateLimited }); this.name = 'NoAccountsError'; this.allRateLimited = allRateLimited; } } /** * Max retries exceeded error */ export class MaxRetriesError extends AntigravityError { /** * @param {string} message - Error message * @param {number} attempts - Number of attempts made */ constructor(message = 'Max retries exceeded', attempts = 0) { super(message, 'MAX_RETRIES', false, { attempts }); this.name = 'MaxRetriesError'; this.attempts = attempts; } } /** * API error from upstream service */ export class ApiError extends AntigravityError { /** * @param {string} message - Error message * @param {number} statusCode - HTTP status code * @param {string} errorType - Type of API error */ constructor(message, statusCode = 500, errorType = 'api_error') { super(message, errorType.toUpperCase(), statusCode >= 500, { statusCode, errorType }); this.name = 'ApiError'; this.statusCode = statusCode; this.errorType = errorType; } } /** * Native module error (version mismatch, rebuild required) */ export class NativeModuleError extends AntigravityError { /** * @param {string} message - Error message * @param {boolean} rebuildSucceeded - Whether auto-rebuild succeeded * @param {boolean} restartRequired - Whether server restart is needed */ constructor(message, rebuildSucceeded = false, restartRequired = false) { super(message, 'NATIVE_MODULE_ERROR', false, { rebuildSucceeded, restartRequired }); this.name = 'NativeModuleError'; this.rebuildSucceeded = rebuildSucceeded; this.restartRequired = restartRequired; } } /** * Empty response error - thrown when API returns no content * Used to trigger retry logic in streaming handler */ export class EmptyResponseError extends AntigravityError { /** * @param {string} message - Error message */ constructor(message = 'No content received from API') { super(message, 'EMPTY_RESPONSE', true, {}); this.name = 'EmptyResponseError'; } } /** * Capacity exhausted error - Google's model is at capacity (not user quota) * Should retry on same account with shorter delay, not switch accounts immediately * Different from QUOTA_EXHAUSTED which indicates user's daily/hourly limit */ export class CapacityExhaustedError extends AntigravityError { /** * @param {string} message - Error message * @param {number|null} retryAfterMs - Suggested retry delay in ms */ constructor(message = 'Model capacity exhausted', retryAfterMs = null) { super(message, 'CAPACITY_EXHAUSTED', true, { retryAfterMs }); this.name = 'CapacityExhaustedError'; this.retryAfterMs = retryAfterMs; } } /** * Account forbidden error (403 VALIDATION_REQUIRED / PERMISSION_DENIED) * These are account-level errors where the account needs validation or has been * disabled. Trying different endpoints won't help - need to rotate to another account. */ export class AccountForbiddenError extends AntigravityError { /** * @param {string} message - Error message * @param {string} accountEmail - Email of the forbidden account */ constructor(message, accountEmail = null) { super(message, 'ACCOUNT_FORBIDDEN', false, { accountEmail }); this.name = 'AccountForbiddenError'; this.accountEmail = accountEmail; } } /** * Check if an error is an account forbidden error (403 VALIDATION_REQUIRED / PERMISSION_DENIED) * These errors indicate the account itself is blocked and need account rotation, not endpoint rotation. * @param {Error} error - Error to check * @returns {boolean} */ export function isAccountForbiddenError(error) { if (error instanceof AccountForbiddenError) return true; // Fallback string check only for errors that couldn't use the typed class // (e.g., errors crossing module boundaries). Only match our own prefixed format. const msg = (error.message || ''); return msg.startsWith('ACCOUNT_FORBIDDEN:'); } /** * Check if an error is a rate limit error * Works with both custom error classes and legacy string-based errors * @param {Error} error - Error to check * @returns {boolean} */ export function isRateLimitError(error) { if (error instanceof RateLimitError) return true; const msg = (error.message || '').toLowerCase(); return msg.includes('429') || msg.includes('resource_exhausted') || msg.includes('quota_exhausted') || msg.includes('rate limit'); } /** * Check if an error is an authentication error * Works with both custom error classes and legacy string-based errors * @param {Error} error - Error to check * @returns {boolean} */ export function isAuthError(error) { if (error instanceof AuthError) return true; const msg = (error.message || '').toUpperCase(); return msg.includes('AUTH_INVALID') || msg.includes('INVALID_GRANT') || msg.includes('TOKEN REFRESH FAILED'); } /** * Check if an error is an empty response error * @param {Error} error - Error to check * @returns {boolean} */ export function isEmptyResponseError(error) { return error instanceof EmptyResponseError || error?.name === 'EmptyResponseError'; } /** * Check if an error is a capacity exhausted error (model overload, not user quota) * This is different from quota exhaustion - capacity issues are temporary infrastructure * limits that should be retried on the SAME account with shorter delays * @param {Error} error - Error to check * @returns {boolean} */ export function isCapacityExhaustedError(error) { if (error instanceof CapacityExhaustedError) return true; const msg = (error.message || '').toLowerCase(); return msg.includes('model_capacity_exhausted') || msg.includes('capacity_exhausted') || msg.includes('model is currently overloaded') || msg.includes('service temporarily unavailable'); } export default { AntigravityError, RateLimitError, AuthError, AccountForbiddenError, NoAccountsError, MaxRetriesError, ApiError, NativeModuleError, EmptyResponseError, CapacityExhaustedError, isRateLimitError, isAuthError, isAccountForbiddenError, isEmptyResponseError, isCapacityExhaustedError };