import { ERROR_CODES, ERROR_MESSAGES } from './constants/errors'; /** * Result of Gemini error analysis */ export interface GeminiErrorResult { code: string; message: string; finishReason?: string | undefined; blockReason?: string | undefined; safetyCategories?: string[] | undefined; retryAfter?: number | undefined; httpStatus?: number | undefined; technicalDetails?: string | undefined; } /** * Safety rating from Gemini response */ interface SafetyRating { category?: string; probability?: string; } /** * Gemini response structure (partial) */ interface GeminiResponse { candidates?: Array<{ finishReason?: string; finishMessage?: string; content?: { parts?: Array<{ text?: string; inlineData?: { data?: string; mimeType?: string }; }>; }; safetyRatings?: SafetyRating[]; }>; promptFeedback?: { blockReason?: string; blockReasonMessage?: string; safetyRatings?: SafetyRating[]; }; usageMetadata?: { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number; }; } /** * Detector for Gemini AI specific errors * Provides detailed error classification for rate limits, safety blocks, timeouts, etc. */ export class GeminiErrorDetector { /** * Classify an API-level error (HTTP errors from Gemini) */ static classifyApiError(error: unknown): GeminiErrorResult { const err = error as { status?: number; message?: string; details?: unknown }; // Check for rate limit (HTTP 429) if (err.status === 429) { const retryAfter = this.extractRetryAfter(error); return { code: ERROR_CODES.GEMINI_RATE_LIMIT, message: retryAfter ? `${ERROR_MESSAGES.GEMINI_RATE_LIMIT}. Retry after ${retryAfter} seconds.` : `${ERROR_MESSAGES.GEMINI_RATE_LIMIT}. Please wait before retrying.`, httpStatus: 429, retryAfter, technicalDetails: err.message, }; } // Check for authentication errors if (err.status === 401 || err.status === 403) { return { code: ERROR_CODES.GEMINI_API_ERROR, message: 'Gemini API authentication failed. Check API key.', httpStatus: err.status, technicalDetails: err.message, }; } // Check for server errors if (err.status === 500 || err.status === 503) { return { code: ERROR_CODES.GEMINI_API_ERROR, message: 'Gemini API service temporarily unavailable.', httpStatus: err.status, technicalDetails: err.message, }; } // Check for bad request if (err.status === 400) { return { code: ERROR_CODES.GEMINI_API_ERROR, message: `Gemini API invalid request: ${err.message || 'Unknown error'}`, httpStatus: 400, technicalDetails: err.message, }; } // Generic API error return { code: ERROR_CODES.GEMINI_API_ERROR, message: err.message || ERROR_MESSAGES.GEMINI_API_ERROR, httpStatus: err.status, technicalDetails: err.message, }; } /** * Analyze a Gemini response for errors (finishReason, blockReason) * Returns null if no error detected */ static analyzeResponse(response: GeminiResponse): GeminiErrorResult | null { // Check promptFeedback for blocked prompts if (response.promptFeedback?.blockReason) { const safetyCategories = this.extractSafetyCategories( response.promptFeedback.safetyRatings ); return { code: ERROR_CODES.GEMINI_CONTENT_BLOCKED, message: response.promptFeedback.blockReasonMessage || `Prompt blocked: ${response.promptFeedback.blockReason}`, blockReason: response.promptFeedback.blockReason, safetyCategories, technicalDetails: `blockReason: ${response.promptFeedback.blockReason}`, }; } // Check candidate finishReason const candidate = response.candidates?.[0]; if (!candidate) { return { code: ERROR_CODES.GEMINI_NO_IMAGE, message: 'No response candidates from Gemini AI.', technicalDetails: 'response.candidates is empty or undefined', }; } const finishReason = candidate.finishReason; // STOP is normal completion if (!finishReason || finishReason === 'STOP') { return null; } // Handle different finishReasons switch (finishReason) { case 'SAFETY': case 'IMAGE_SAFETY': { const safetyCategories = this.extractSafetyCategories(candidate.safetyRatings); return { code: ERROR_CODES.GEMINI_SAFETY_BLOCK, message: `Content blocked due to safety: ${safetyCategories.join(', ') || 'unspecified'}`, finishReason, safetyCategories, technicalDetails: `finishReason: ${finishReason}, safetyRatings: ${JSON.stringify(candidate.safetyRatings)}`, }; } case 'NO_IMAGE': return { code: ERROR_CODES.GEMINI_NO_IMAGE, message: 'Gemini AI could not generate an image for this prompt. Try rephrasing.', finishReason, technicalDetails: `finishReason: ${finishReason}`, }; case 'IMAGE_PROHIBITED_CONTENT': return { code: ERROR_CODES.GEMINI_CONTENT_BLOCKED, message: 'Image generation blocked due to prohibited content in prompt.', finishReason, technicalDetails: `finishReason: ${finishReason}`, }; case 'MAX_TOKENS': return { code: ERROR_CODES.GEMINI_API_ERROR, message: 'Response exceeded maximum token limit. Try a shorter prompt.', finishReason, technicalDetails: `finishReason: ${finishReason}`, }; case 'RECITATION': case 'IMAGE_RECITATION': return { code: ERROR_CODES.GEMINI_CONTENT_BLOCKED, message: 'Response blocked due to potential copyright concerns.', finishReason, technicalDetails: `finishReason: ${finishReason}`, }; default: return { code: ERROR_CODES.GEMINI_API_ERROR, message: `Generation stopped unexpectedly: ${finishReason}`, finishReason, technicalDetails: `finishReason: ${finishReason}, finishMessage: ${candidate.finishMessage}`, }; } } /** * Check if response has image data */ static hasImageData(response: GeminiResponse): boolean { const parts = response.candidates?.[0]?.content?.parts; if (!parts) return false; return parts.some((part) => part.inlineData?.data); } /** * Format error result for logging */ static formatForLogging(result: GeminiErrorResult): string { const parts = [`[${result.code}] ${result.message}`]; if (result.finishReason) { parts.push(`finishReason=${result.finishReason}`); } if (result.blockReason) { parts.push(`blockReason=${result.blockReason}`); } if (result.httpStatus) { parts.push(`httpStatus=${result.httpStatus}`); } if (result.retryAfter) { parts.push(`retryAfter=${result.retryAfter}s`); } if (result.safetyCategories?.length) { parts.push(`safety=[${result.safetyCategories.join(', ')}]`); } return parts.join(' | '); } /** * Log Gemini response structure for debugging */ static logResponseStructure(response: GeminiResponse, prefix: string = ''): void { const parts = response.candidates?.[0]?.content?.parts || []; const partTypes = parts.map((p) => { if (p.inlineData) return 'image'; if (p.text) return 'text'; return 'other'; }); console.log(`[ImageGenService]${prefix ? ` [${prefix}]` : ''} Gemini response:`, { hasCandidates: !!response.candidates?.length, candidateCount: response.candidates?.length || 0, finishReason: response.candidates?.[0]?.finishReason || null, blockReason: response.promptFeedback?.blockReason || null, partsCount: parts.length, partTypes, usageMetadata: response.usageMetadata || null, }); } /** * Extract retry-after value from error */ private static extractRetryAfter(error: unknown): number | undefined { const err = error as { headers?: { get?: (key: string) => string | null } }; // Try to get from headers if (err.headers?.get) { const retryAfter = err.headers.get('retry-after'); if (retryAfter) { const seconds = parseInt(retryAfter, 10); if (!isNaN(seconds)) return seconds; } } // Default retry after for rate limits return 60; } /** * Extract safety category names from ratings */ private static extractSafetyCategories(ratings?: SafetyRating[]): string[] { if (!ratings || ratings.length === 0) return []; // Filter for high/medium probability ratings and extract category names return ratings .filter((r) => r.probability === 'HIGH' || r.probability === 'MEDIUM') .map((r) => r.category?.replace('HARM_CATEGORY_', '') || 'UNKNOWN') .filter((c) => c !== 'UNKNOWN'); } }