feat: implement retry logic for service unavailable errors in AI content generation
This commit is contained in:
@@ -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 {
|
export class AIService {
|
||||||
private genAI: GoogleGenerativeAI;
|
private genAI: GoogleGenerativeAI;
|
||||||
private genAIv2: any;
|
private genAIv2: any;
|
||||||
@@ -412,6 +452,9 @@ export class AIService {
|
|||||||
if (apiMessage.includes('quota') || apiMessage.includes('exceeded')) {
|
if (apiMessage.includes('quota') || apiMessage.includes('exceeded')) {
|
||||||
return 'Se ha alcanzado el límite de uso de la API. Intenta más tarde';
|
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')) {
|
if (apiMessage.includes('safety') || apiMessage.includes('blocked')) {
|
||||||
return 'Tu mensaje fue bloqueado por las políticas de seguridad';
|
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')) {
|
if (message.includes('quota') || message.includes('exceeded')) {
|
||||||
return 'Se ha alcanzado el límite de uso de la API. Intenta más tarde';
|
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')) {
|
if (message.includes('safety') || message.includes('blocked')) {
|
||||||
return 'Tu mensaje fue bloqueado por las políticas de seguridad';
|
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';
|
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
|
* 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
|
// Usar gemini-2.5-flash-preview-09-2025 que puede leer imágenes y responder con texto
|
||||||
const model = this.genAI.getGenerativeModel({
|
const model = this.genAI.getGenerativeModel({
|
||||||
model: "gemini-2.5-flash-preview-09-2025",
|
model: "gemini-2.5-flash",
|
||||||
generationConfig: {
|
generationConfig: {
|
||||||
maxOutputTokens: Math.min(this.config.maxOutputTokens, Math.max(1024, estimatedTokens * 0.5)),
|
maxOutputTokens: Math.min(this.config.maxOutputTokens, Math.max(1024, estimatedTokens * 0.5)),
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
@@ -729,7 +816,7 @@ export class AIService {
|
|||||||
content = systemPrompt;
|
content = systemPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await model.generateContent(content);
|
const result = await this.generateContentWithRetries(model, content);
|
||||||
const response = await result.response;
|
const response = await result.response;
|
||||||
const aiResponse = response.text()?.trim();
|
const aiResponse = response.text()?.trim();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user