/** * AI Error Classification Service * * Provides unified error handling and classification for all AI services. * Converts technical API errors into user-friendly messages. */ // ─── Error Types ───────────────────────────────────────────────────────────── export type AIErrorType = | 'rate_limit' | 'quota_exceeded' | 'model_not_found' | 'invalid_api_key' | 'context_length' | 'parse_error' | 'timeout' | 'network_error' | 'content_filter' | 'server_error' | 'unknown' export interface ClassifiedError { type: AIErrorType message: string originalMessage: string retryable: boolean suggestedAction?: string } // ─── Error Patterns ────────────────────────────────────────────────────────── interface ErrorPattern { type: AIErrorType patterns: Array retryable: boolean userMessage: string suggestedAction?: string } const ERROR_PATTERNS: ErrorPattern[] = [ { type: 'rate_limit', patterns: [ 'rate_limit', 'rate limit', 'too many requests', '429', 'quota exceeded', 'Rate limit reached', ], retryable: true, userMessage: 'Rate limit exceeded. Please wait a few minutes and try again.', suggestedAction: 'Wait 1-2 minutes before retrying, or reduce batch size.', }, { type: 'quota_exceeded', patterns: [ 'insufficient_quota', 'billing', 'exceeded your current quota', 'payment required', 'account deactivated', ], retryable: false, userMessage: 'API quota exceeded. Please check your OpenAI billing settings.', suggestedAction: 'Add payment method or increase spending limit in OpenAI dashboard.', }, { type: 'model_not_found', patterns: [ 'model_not_found', 'does not exist', 'The model', 'invalid model', 'model not available', ], retryable: false, userMessage: 'The selected AI model is not available. Please check your settings.', suggestedAction: 'Go to Settings → AI and select a different model.', }, { type: 'invalid_api_key', patterns: [ 'invalid_api_key', 'Incorrect API key', 'authentication', 'unauthorized', '401', 'invalid api key', ], retryable: false, userMessage: 'Invalid API key. Please check your OpenAI API key in settings.', suggestedAction: 'Go to Settings → AI and enter a valid API key.', }, { type: 'context_length', patterns: [ 'context_length', 'maximum context length', 'tokens', 'too long', 'reduce the length', 'max_tokens', ], retryable: true, userMessage: 'Request too large. Try processing fewer items at once.', suggestedAction: 'Process items in smaller batches.', }, { type: 'content_filter', patterns: [ 'content_filter', 'content policy', 'flagged', 'inappropriate', 'safety system', ], retryable: false, userMessage: 'Content was flagged by the AI safety system. Please review the input data.', suggestedAction: 'Check project descriptions for potentially sensitive content.', }, { type: 'timeout', patterns: [ 'timeout', 'timed out', 'ETIMEDOUT', 'ECONNABORTED', 'deadline exceeded', ], retryable: true, userMessage: 'Request timed out. Please try again.', suggestedAction: 'Try again or process fewer items at once.', }, { type: 'network_error', patterns: [ 'ENOTFOUND', 'ECONNREFUSED', 'network', 'connection', 'DNS', 'getaddrinfo', ], retryable: true, userMessage: 'Network error. Please check your connection and try again.', suggestedAction: 'Check network connectivity and firewall settings.', }, { type: 'server_error', patterns: [ '500', '502', '503', '504', 'internal error', 'server error', 'service unavailable', ], retryable: true, userMessage: 'OpenAI service temporarily unavailable. Please try again later.', suggestedAction: 'Wait a few minutes and retry. Check status.openai.com for outages.', }, ] // ─── Error Classification ──────────────────────────────────────────────────── /** * Classify an error from the OpenAI API */ export function classifyAIError(error: Error | unknown): ClassifiedError { const errorMessage = error instanceof Error ? error.message : String(error) const errorString = errorMessage.toLowerCase() // Check against known patterns for (const pattern of ERROR_PATTERNS) { for (const matcher of pattern.patterns) { const matches = typeof matcher === 'string' ? errorString.includes(matcher.toLowerCase()) : matcher.test(errorString) if (matches) { return { type: pattern.type, message: pattern.userMessage, originalMessage: errorMessage, retryable: pattern.retryable, suggestedAction: pattern.suggestedAction, } } } } // Unknown error return { type: 'unknown', message: 'An unexpected error occurred. Please try again.', originalMessage: errorMessage, retryable: true, suggestedAction: 'If the problem persists, check the AI settings or contact support.', } } /** * Check if an error is a JSON parse error */ export function isParseError(error: Error | unknown): boolean { const message = error instanceof Error ? error.message : String(error) return ( message.includes('JSON') || message.includes('parse') || message.includes('Unexpected token') || message.includes('SyntaxError') ) } /** * Create a classified parse error */ export function createParseError(originalMessage: string): ClassifiedError { return { type: 'parse_error', message: 'AI returned an invalid response. Items flagged for manual review.', originalMessage, retryable: true, suggestedAction: 'Review flagged items manually. Consider using a different model.', } } // ─── User-Friendly Messages ────────────────────────────────────────────────── const USER_FRIENDLY_MESSAGES: Record = { rate_limit: 'Rate limit exceeded. Please wait a few minutes and try again.', quota_exceeded: 'API quota exceeded. Please check your OpenAI billing settings.', model_not_found: 'Selected AI model is not available. Please check your settings.', invalid_api_key: 'Invalid API key. Please verify your OpenAI API key.', context_length: 'Request too large. Please try with fewer items.', parse_error: 'AI response could not be processed. Items flagged for review.', timeout: 'Request timed out. Please try again.', network_error: 'Network connection error. Please check your connection.', content_filter: 'Content flagged by AI safety system. Please review input data.', server_error: 'AI service temporarily unavailable. Please try again later.', unknown: 'An unexpected error occurred. Please try again.', } /** * Get a user-friendly message for an error type */ export function getUserFriendlyMessage(errorType: AIErrorType): string { return USER_FRIENDLY_MESSAGES[errorType] } // ─── Error Handling Helpers ────────────────────────────────────────────────── /** * Wrap an async function with standardized AI error handling */ export async function withAIErrorHandling( fn: () => Promise, fallback: T ): Promise<{ result: T; error?: ClassifiedError }> { try { const result = await fn() return { result } } catch (error) { const classified = classifyAIError(error) console.error(`[AI Error] ${classified.type}:`, classified.originalMessage) return { result: fallback, error: classified } } } /** * Log an AI error with context */ export function logAIError( service: string, operation: string, error: ClassifiedError, context?: Record ): void { console.error( `[AI ${service}] ${operation} failed:`, JSON.stringify({ type: error.type, message: error.message, originalMessage: error.originalMessage, retryable: error.retryable, ...context, }) ) } // ─── Retry Logic ───────────────────────────────────────────────────────────── /** * Determine if an operation should be retried based on error type */ export function shouldRetry(error: ClassifiedError, attempt: number, maxAttempts: number = 3): boolean { if (!error.retryable) return false if (attempt >= maxAttempts) return false // Rate limits need longer delays if (error.type === 'rate_limit') { return attempt < 2 // Only retry once for rate limits } return true } /** * Calculate delay before retry (exponential backoff) */ export function getRetryDelay(error: ClassifiedError, attempt: number): number { const baseDelay = error.type === 'rate_limit' ? 30000 : 1000 // 30s for rate limit, 1s otherwise return baseDelay * Math.pow(2, attempt) }