feat: implement retry logic for service unavailable errors in AI content generation

This commit is contained in:
2025-10-06 10:28:01 -05:00
parent 1e912be72c
commit 10e9cdd092

View File

@@ -83,6 +83,46 @@ function isAPIError(error: unknown): error is { message: string; code?: string }
);
}
const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
function isServiceUnavailableError(error: unknown): boolean {
if (!error) {
return false;
}
const message = getErrorMessage(error).toLowerCase();
if (
message.includes('503') ||
message.includes('service unavailable') ||
message.includes('model is overloaded') ||
message.includes('model estuvo sobrecargado') ||
message.includes('overloaded') ||
message.includes('temporarily unavailable')
) {
return true;
}
const status = (error as any)?.status ?? (error as any)?.statusCode ?? (error as any)?.code;
if (typeof status === 'number' && status === 503) {
return true;
}
if (typeof status === 'string' && status.includes('503')) {
return true;
}
if (isAPIError(error)) {
const apiMessage = error.message.toLowerCase();
return (
apiMessage.includes('503') ||
apiMessage.includes('service unavailable') ||
apiMessage.includes('overloaded')
);
}
return false;
}
export class AIService {
private genAI: GoogleGenerativeAI;
private genAIv2: any;
@@ -412,6 +452,9 @@ export class AIService {
if (apiMessage.includes('quota') || apiMessage.includes('exceeded')) {
return 'Se ha alcanzado el límite de uso de la API. Intenta más tarde';
}
if (apiMessage.includes('service unavailable') || apiMessage.includes('overloaded') || apiMessage.includes('503')) {
return 'El servicio de IA está saturado. Intenta de nuevo en unos segundos';
}
if (apiMessage.includes('safety') || apiMessage.includes('blocked')) {
return 'Tu mensaje fue bloqueado por las políticas de seguridad';
}
@@ -433,6 +476,9 @@ export class AIService {
if (message.includes('quota') || message.includes('exceeded')) {
return 'Se ha alcanzado el límite de uso de la API. Intenta más tarde';
}
if (message.includes('service unavailable') || message.includes('overloaded') || message.includes('503')) {
return 'El servicio de IA está saturado. Intenta de nuevo en unos segundos';
}
if (message.includes('safety') || message.includes('blocked')) {
return 'Tu mensaje fue bloqueado por las políticas de seguridad';
}
@@ -449,6 +495,47 @@ export class AIService {
return 'Error temporal del servicio de IA. Intenta de nuevo';
}
private async generateContentWithRetries(model: any, content: any, options?: {
maxAttempts?: number;
baseDelayMs?: number;
maxDelayMs?: number;
}): Promise<any> {
const {
maxAttempts = 3,
baseDelayMs = 1200,
maxDelayMs = 10_000
} = options ?? {};
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await model.generateContent(content);
} catch (error) {
lastError = error;
const isRetryable = isServiceUnavailableError(error);
const isLastAttempt = attempt === maxAttempts - 1;
if (!isRetryable || isLastAttempt) {
throw error;
}
const backoff = Math.min(maxDelayMs, Math.floor(baseDelayMs * Math.pow(2, attempt)));
const jitter = Math.floor(Math.random() * Math.max(200, Math.floor(baseDelayMs / 2)));
const waitMs = backoff + jitter;
logger.warn(
{ attempt: attempt + 1, waitMs },
`Gemini respondió 503 (overloaded). Reintentando en ${waitMs}ms (intento ${attempt + 2}/${maxAttempts})`
);
await sleep(waitMs);
}
}
throw lastError ?? new Error('Error desconocido al generar contenido con Gemini');
}
/**
* Procesa una request de IA con soporte para conversaciones y memoria persistente
*/
@@ -696,7 +783,7 @@ export class AIService {
// Usar gemini-2.5-flash-preview-09-2025 que puede leer imágenes y responder con texto
const model = this.genAI.getGenerativeModel({
model: "gemini-2.5-flash-preview-09-2025",
model: "gemini-2.5-flash",
generationConfig: {
maxOutputTokens: Math.min(this.config.maxOutputTokens, Math.max(1024, estimatedTokens * 0.5)),
temperature: 0.7,
@@ -729,7 +816,7 @@ export class AIService {
content = systemPrompt;
}
const result = await model.generateContent(content);
const result = await this.generateContentWithRetries(model, content);
const response = await result.response;
const aiResponse = response.text()?.trim();