feat: log errors transparent
This commit is contained in:
parent
861aa325e4
commit
855ac3c111
|
|
@ -12,10 +12,13 @@ import {
|
||||||
import { StorageFactory } from './StorageFactory';
|
import { StorageFactory } from './StorageFactory';
|
||||||
import { TTILogger, TTILogEntry } from './TTILogger';
|
import { TTILogger, TTILogEntry } from './TTILogger';
|
||||||
import { NetworkErrorDetector } from '../utils/NetworkErrorDetector';
|
import { NetworkErrorDetector } from '../utils/NetworkErrorDetector';
|
||||||
|
import { GeminiErrorDetector } from '../utils/GeminiErrorDetector';
|
||||||
|
import { ERROR_MESSAGES } from '../utils/constants/errors';
|
||||||
|
|
||||||
export class ImageGenService {
|
export class ImageGenService {
|
||||||
private ai: GoogleGenAI;
|
private ai: GoogleGenAI;
|
||||||
private primaryModel = 'gemini-2.5-flash-image';
|
private primaryModel = 'gemini-2.5-flash-image';
|
||||||
|
private static GEMINI_TIMEOUT_MS = 90_000; // 90 seconds
|
||||||
|
|
||||||
constructor(apiKey: string) {
|
constructor(apiKey: string) {
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
|
|
@ -205,18 +208,56 @@ export class ImageGenService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the EXACT same config and contents objects calculated above
|
// Use the EXACT same config and contents objects calculated above
|
||||||
const response = await this.ai.models.generateContent({
|
// Wrap with timeout to prevent hanging requests
|
||||||
model: this.primaryModel,
|
const response = await this.withTimeout(
|
||||||
config,
|
this.ai.models.generateContent({
|
||||||
contents,
|
model: this.primaryModel,
|
||||||
});
|
config,
|
||||||
|
contents,
|
||||||
|
}),
|
||||||
|
ImageGenService.GEMINI_TIMEOUT_MS,
|
||||||
|
'Gemini image generation'
|
||||||
|
);
|
||||||
|
|
||||||
// Parse response
|
// Log response structure for debugging
|
||||||
if (!response.candidates || !response.candidates[0] || !response.candidates[0].content) {
|
GeminiErrorDetector.logResponseStructure(response as any);
|
||||||
throw new Error('No response received from Gemini AI');
|
|
||||||
|
// Check promptFeedback for blocked prompts FIRST
|
||||||
|
if ((response as any).promptFeedback?.blockReason) {
|
||||||
|
const errorResult = GeminiErrorDetector.analyzeResponse(response as any);
|
||||||
|
console.error(
|
||||||
|
`[ImageGenService] Prompt blocked:`,
|
||||||
|
GeminiErrorDetector.formatForLogging(errorResult!)
|
||||||
|
);
|
||||||
|
throw new Error(errorResult!.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = response.candidates[0].content;
|
// Check if we have candidates
|
||||||
|
if (!response.candidates || !response.candidates[0]) {
|
||||||
|
const errorResult = GeminiErrorDetector.analyzeResponse(response as any);
|
||||||
|
console.error(`[ImageGenService] No candidates in response`);
|
||||||
|
throw new Error(errorResult?.message || 'No response candidates from Gemini AI');
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = response.candidates[0];
|
||||||
|
|
||||||
|
// Check finishReason for non-STOP completions
|
||||||
|
if (candidate.finishReason && candidate.finishReason !== 'STOP') {
|
||||||
|
const errorResult = GeminiErrorDetector.analyzeResponse(response as any);
|
||||||
|
console.error(
|
||||||
|
`[ImageGenService] Non-STOP finish reason:`,
|
||||||
|
GeminiErrorDetector.formatForLogging(errorResult!)
|
||||||
|
);
|
||||||
|
throw new Error(errorResult!.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check content exists
|
||||||
|
if (!candidate.content) {
|
||||||
|
console.error(`[ImageGenService] No content in candidate`);
|
||||||
|
throw new Error('No content in Gemini AI response');
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = candidate.content;
|
||||||
let generatedDescription: string | undefined;
|
let generatedDescription: string | undefined;
|
||||||
let imageData: { buffer: Buffer; mimeType: string } | null = null;
|
let imageData: { buffer: Buffer; mimeType: string } | null = null;
|
||||||
|
|
||||||
|
|
@ -232,7 +273,14 @@ export class ImageGenService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!imageData) {
|
if (!imageData) {
|
||||||
throw new Error('No image data received from Gemini AI');
|
// Log what we got instead of image
|
||||||
|
const partTypes = (content.parts || []).map((p: any) =>
|
||||||
|
p.inlineData ? 'image' : p.text ? 'text' : 'other'
|
||||||
|
);
|
||||||
|
console.error(`[ImageGenService] No image data in response. Parts: [${partTypes.join(', ')}]`);
|
||||||
|
throw new Error(
|
||||||
|
`${ERROR_MESSAGES.GEMINI_NO_IMAGE}. Response contained: ${partTypes.join(', ') || 'nothing'}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileExtension = mime.getExtension(imageData.mimeType) || 'png';
|
const fileExtension = mime.getExtension(imageData.mimeType) || 'png';
|
||||||
|
|
@ -264,6 +312,38 @@ export class ImageGenService {
|
||||||
geminiParams,
|
geminiParams,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Check for rate limit (HTTP 429)
|
||||||
|
const err = error as { status?: number; message?: string };
|
||||||
|
if (err.status === 429) {
|
||||||
|
const geminiError = GeminiErrorDetector.classifyApiError(error);
|
||||||
|
console.error(
|
||||||
|
`[ImageGenService] Rate limit:`,
|
||||||
|
GeminiErrorDetector.formatForLogging(geminiError)
|
||||||
|
);
|
||||||
|
throw new Error(geminiError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for timeout
|
||||||
|
if (error instanceof Error && error.message.includes('timed out')) {
|
||||||
|
console.error(
|
||||||
|
`[ImageGenService] Timeout after ${ImageGenService.GEMINI_TIMEOUT_MS}ms:`,
|
||||||
|
error.message
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`${ERROR_MESSAGES.GEMINI_TIMEOUT} after ${ImageGenService.GEMINI_TIMEOUT_MS / 1000} seconds`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for other API errors with status codes
|
||||||
|
if (err.status) {
|
||||||
|
const geminiError = GeminiErrorDetector.classifyApiError(error);
|
||||||
|
console.error(
|
||||||
|
`[ImageGenService] API error:`,
|
||||||
|
GeminiErrorDetector.formatForLogging(geminiError)
|
||||||
|
);
|
||||||
|
throw new Error(geminiError.message);
|
||||||
|
}
|
||||||
|
|
||||||
// Enhanced error detection with network diagnostics
|
// Enhanced error detection with network diagnostics
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
// Classify the error and check for network issues (only on failure)
|
// Classify the error and check for network issues (only on failure)
|
||||||
|
|
@ -279,6 +359,32 @@ export class ImageGenService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a promise with timeout
|
||||||
|
*/
|
||||||
|
private async withTimeout<T>(
|
||||||
|
promise: Promise<T>,
|
||||||
|
timeoutMs: number,
|
||||||
|
operationName: string
|
||||||
|
): Promise<T> {
|
||||||
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
reject(new Error(`${operationName} timed out after ${timeoutMs}ms`));
|
||||||
|
}, timeoutMs);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await Promise.race([promise, timeoutPromise]);
|
||||||
|
clearTimeout(timeoutId!);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId!);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static validateReferenceImages(files: Express.Multer.File[]): {
|
static validateReferenceImages(files: Express.Multer.File[]): {
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ export interface ImageGenerationResult {
|
||||||
model: string;
|
model: string;
|
||||||
geminiParams?: GeminiParams; // Gemini SDK parameters used for generation
|
geminiParams?: GeminiParams; // Gemini SDK parameters used for generation
|
||||||
error?: string;
|
error?: string;
|
||||||
|
errorCode?: string; // Gemini-specific error code (GEMINI_RATE_LIMIT, GEMINI_TIMEOUT, etc.)
|
||||||
errorType?: 'generation' | 'storage'; // Distinguish between generation and storage errors
|
errorType?: 'generation' | 'storage'; // Distinguish between generation and storage errors
|
||||||
generatedImageData?: GeneratedImageData; // Available when generation succeeds but storage fails
|
generatedImageData?: GeneratedImageData; // Available when generation succeeds but storage fails
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,14 @@ export const ERROR_MESSAGES = {
|
||||||
INTERNAL_SERVER_ERROR: 'Internal server error',
|
INTERNAL_SERVER_ERROR: 'Internal server error',
|
||||||
INVALID_REQUEST: 'Invalid request',
|
INVALID_REQUEST: 'Invalid request',
|
||||||
OPERATION_FAILED: 'Operation failed',
|
OPERATION_FAILED: 'Operation failed',
|
||||||
|
|
||||||
|
// Gemini AI Errors
|
||||||
|
GEMINI_RATE_LIMIT: 'Gemini API rate limit exceeded',
|
||||||
|
GEMINI_CONTENT_BLOCKED: 'Content blocked by Gemini safety filters',
|
||||||
|
GEMINI_TIMEOUT: 'Gemini API request timed out',
|
||||||
|
GEMINI_NO_IMAGE: 'Gemini AI could not generate image',
|
||||||
|
GEMINI_SAFETY_BLOCK: 'Content blocked due to safety concerns',
|
||||||
|
GEMINI_API_ERROR: 'Gemini API returned an error',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ERROR_CODES = {
|
export const ERROR_CODES = {
|
||||||
|
|
@ -109,6 +117,14 @@ export const ERROR_CODES = {
|
||||||
INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
|
INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR',
|
||||||
INVALID_REQUEST: 'INVALID_REQUEST',
|
INVALID_REQUEST: 'INVALID_REQUEST',
|
||||||
OPERATION_FAILED: 'OPERATION_FAILED',
|
OPERATION_FAILED: 'OPERATION_FAILED',
|
||||||
|
|
||||||
|
// Gemini AI Errors
|
||||||
|
GEMINI_RATE_LIMIT: 'GEMINI_RATE_LIMIT',
|
||||||
|
GEMINI_CONTENT_BLOCKED: 'GEMINI_CONTENT_BLOCKED',
|
||||||
|
GEMINI_TIMEOUT: 'GEMINI_TIMEOUT',
|
||||||
|
GEMINI_NO_IMAGE: 'GEMINI_NO_IMAGE',
|
||||||
|
GEMINI_SAFETY_BLOCK: 'GEMINI_SAFETY_BLOCK',
|
||||||
|
GEMINI_API_ERROR: 'GEMINI_API_ERROR',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue