| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import type { ErrorType, ErrorInfo } from '@automaker/types'; |
|
|
| |
| |
| |
| |
| |
| |
| export function isAbortError(error: unknown): boolean { |
| return error instanceof Error && (error.name === 'AbortError' || error.message.includes('abort')); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function isCancellationError(errorMessage: string): boolean { |
| const lowerMessage = errorMessage.toLowerCase(); |
| return ( |
| lowerMessage.includes('cancelled') || |
| lowerMessage.includes('canceled') || |
| lowerMessage.includes('stopped') || |
| lowerMessage.includes('aborted') |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function isAuthenticationError(errorMessage: string): boolean { |
| return ( |
| errorMessage.includes('Authentication failed') || |
| errorMessage.includes('Invalid API key') || |
| errorMessage.includes('authentication_failed') || |
| errorMessage.includes('Fix external API key') |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function isRateLimitError(error: unknown): boolean { |
| const message = error instanceof Error ? error.message : String(error || ''); |
| return message.includes('429') || message.includes('rate_limit'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function isQuotaExhaustedError(error: unknown): boolean { |
| const message = error instanceof Error ? error.message : String(error || ''); |
| const lowerMessage = message.toLowerCase(); |
|
|
| |
| if ( |
| lowerMessage.includes('overloaded') || |
| lowerMessage.includes('overloaded_error') || |
| lowerMessage.includes('capacity') |
| ) { |
| return true; |
| } |
|
|
| |
| if ( |
| lowerMessage.includes('limit reached') || |
| lowerMessage.includes('usage limit') || |
| lowerMessage.includes('quota exceeded') || |
| lowerMessage.includes('quota_exceeded') || |
| lowerMessage.includes('session limit') || |
| lowerMessage.includes('weekly limit') || |
| lowerMessage.includes('monthly limit') |
| ) { |
| return true; |
| } |
|
|
| |
| if ( |
| lowerMessage.includes('credit balance') || |
| lowerMessage.includes('insufficient credits') || |
| lowerMessage.includes('insufficient balance') || |
| lowerMessage.includes('no credits') || |
| lowerMessage.includes('out of credits') || |
| lowerMessage.includes('billing') || |
| lowerMessage.includes('payment required') |
| ) { |
| return true; |
| } |
|
|
| |
| if (lowerMessage.includes('/upgrade') || lowerMessage.includes('extra-usage')) { |
| return true; |
| } |
|
|
| return false; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function isModelNotFoundError(error: unknown): boolean { |
| const message = error instanceof Error ? error.message : String(error || ''); |
| const lowerMessage = message.toLowerCase(); |
|
|
| return ( |
| lowerMessage.includes('does not exist or you do not have access') || |
| lowerMessage.includes('model_not_found') || |
| lowerMessage.includes('invalid_model') || |
| (lowerMessage.includes('model') && |
| (lowerMessage.includes('does not exist') || lowerMessage.includes('not found'))) |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function isStreamDisconnectedError(error: unknown): boolean { |
| const message = error instanceof Error ? error.message : String(error || ''); |
| const lowerMessage = message.toLowerCase(); |
|
|
| return ( |
| lowerMessage.includes('stream disconnected') || |
| lowerMessage.includes('stream ended') || |
| lowerMessage.includes('connection reset') || |
| lowerMessage.includes('socket hang up') || |
| lowerMessage.includes('econnreset') |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function extractRetryAfter(error: unknown): number | undefined { |
| const message = error instanceof Error ? error.message : String(error || ''); |
|
|
| |
| const retryMatch = message.match(/retry[_-]?after[:\s]+(\d+)/i); |
| if (retryMatch) { |
| return parseInt(retryMatch[1], 10); |
| } |
|
|
| |
| const waitMatch = message.match(/wait[:\s]+(\d+)\s*(?:second|sec|s)/i); |
| if (waitMatch) { |
| return parseInt(waitMatch[1], 10); |
| } |
|
|
| return undefined; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function classifyError(error: unknown): ErrorInfo { |
| const message = error instanceof Error ? error.message : String(error || 'Unknown error'); |
| const isAbort = isAbortError(error); |
| const isAuth = isAuthenticationError(message); |
| const isCancellation = isCancellationError(message); |
| const isRateLimit = isRateLimitError(error); |
| const isQuotaExhausted = isQuotaExhaustedError(error); |
| const isModelNotFound = isModelNotFoundError(error); |
| const isStreamDisconnected = isStreamDisconnectedError(error); |
| const retryAfter = isRateLimit ? (extractRetryAfter(error) ?? 60) : undefined; |
|
|
| let type: ErrorType; |
| if (isAuth) { |
| type = 'authentication'; |
| } else if (isModelNotFound) { |
| type = 'model_not_found'; |
| } else if (isStreamDisconnected) { |
| type = 'stream_disconnected'; |
| } else if (isQuotaExhausted) { |
| |
| type = 'quota_exhausted'; |
| } else if (isRateLimit) { |
| type = 'rate_limit'; |
| } else if (isAbort) { |
| type = 'abort'; |
| } else if (isCancellation) { |
| type = 'cancellation'; |
| } else if (error instanceof Error) { |
| type = 'execution'; |
| } else { |
| type = 'unknown'; |
| } |
|
|
| return { |
| type, |
| message, |
| isAbort, |
| isAuth, |
| isCancellation, |
| isRateLimit, |
| isQuotaExhausted, |
| isModelNotFound, |
| isStreamDisconnected, |
| retryAfter, |
| originalError: error, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function getUserFriendlyErrorMessage(error: unknown): string { |
| const info = classifyError(error); |
|
|
| if (info.isAbort) { |
| return 'Operation was cancelled'; |
| } |
|
|
| if (info.isAuth) { |
| return 'Authentication failed. Please check your API key.'; |
| } |
|
|
| if (info.isModelNotFound) { |
| return `Model not available: ${info.message}\n\nSome models require specific subscription plans or authentication methods. Try authenticating with 'codex login' or switch to a different model.`; |
| } |
|
|
| if (info.isStreamDisconnected) { |
| return `Connection interrupted: ${info.message}\n\nThe stream was disconnected before the response could complete. This may be caused by network issues, model access restrictions, or server timeouts. Try again or switch to a different model.`; |
| } |
|
|
| if (info.isQuotaExhausted) { |
| return 'Usage limit reached. Auto Mode has been paused. Please wait for your quota to reset or upgrade your plan.'; |
| } |
|
|
| if (info.isRateLimit) { |
| const retryMsg = info.retryAfter |
| ? ` Please wait ${info.retryAfter} seconds before retrying.` |
| : ' Please reduce concurrency or wait before retrying.'; |
| return `Rate limit exceeded (429).${retryMsg}`; |
| } |
|
|
| return info.message; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function getErrorMessage(error: unknown): string { |
| return error instanceof Error ? error.message : 'Unknown error'; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function logError(error: unknown, context: string): void { |
| console.error(`❌ ${context}:`, error); |
| } |
|
|