diff --git a/apps/api/commands/ocr_validate.ts b/apps/api/commands/ocr_validate.ts index 9ee7479..1b4d1ee 100644 --- a/apps/api/commands/ocr_validate.ts +++ b/apps/api/commands/ocr_validate.ts @@ -171,17 +171,32 @@ export default class OcrValidate extends BaseCommand { await driveDisk.put(storageKey, buffer) const t0 = Date.now() - let ocrResult: OcrResult - try { - ocrResult = await provider.extract({ storageKey, filename: pdf }) - } catch (err) { - this.logger.error(`✗ ${pdf} — extraction throw : ${(err as Error).message}`) - // Cleanup temp file - await driveDisk.delete(storageKey).catch(() => {}) - continue + let ocrResult: OcrResult | null = null + // Retry sur 429 (rate limit) avec backoff exponentiel — utile en + // free tier Mistral où la limite n'est pas linéaire. Max 3 retries + // (30s + 60s + 90s = 3 min max d'attente avant abandon). + const maxRetries = 3 + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + ocrResult = await provider.extract({ storageKey, filename: pdf }) + break + } catch (err) { + const msg = (err as Error).message + const isRateLimit = msg.includes('429') || msg.includes('Rate limit') + if (!isRateLimit || attempt === maxRetries) { + this.logger.error(`✗ ${pdf} — extraction throw : ${msg}`) + break + } + const waitSec = 30 + attempt * 30 + this.logger.warning( + `⏸ ${pdf} — 429 rate limit, retry dans ${waitSec}s (attempt ${attempt + 1}/${maxRetries})` + ) + await new Promise((r) => setTimeout(r, waitSec * 1000)) + } } const durationMs = Date.now() - t0 await driveDisk.delete(storageKey).catch(() => {}) + if (!ocrResult) continue const comparisons = compareFields(expected, ocrResult) const allMatch = comparisons.every((c) => c.match)