diff --git a/api/index.php b/api/index.php index 8cae7d4..0c7f332 100644 --- a/api/index.php +++ b/api/index.php @@ -5414,6 +5414,7 @@ function saveSettings(): void { */ function callGemini(string $url, array $payload, int $timeout = 60): array { $maxAttempts = 4; + $maxTotalElapsed = 45; // budget de sécurité pour ne jamais approcher le set_time_limit(120) de PHP $lastCode = 0; $lastBody = ''; $promptLen = strlen(json_encode($payload)); @@ -5445,26 +5446,38 @@ function callGemini(string $url, array $payload, int $timeout = 60): array { if ($body !== false) $lastBody = $body; - // Success or non-retryable error → stop immediately +// Success or non-retryable error → stop immediately if ($lastCode === 200) break; if ($lastCode !== 429 && $lastCode !== 503) break; if ($attempt >= $maxAttempts) break; + // Quota épuisé (RESOURCE_EXHAUSTED) n'est PAS transitoire — retenter le même modèle + // dans cette requête ne servira à rien. On échoue vite plutôt que de risquer que PHP + // tue le script en plein milieu (ce qui remonte comme un 502/504 moche côté client). + $errData = $body ? json_decode($body, true) : null; + if (($errData['error']['status'] ?? '') === 'RESOURCE_EXHAUSTED') { + EverLog::warn('AI quota exhausted, failing fast', ['code' => $lastCode]); + break; + } + // Determine how long to wait ----------------------------------------------- // Priority 1: Retry-After header (set by Google in some 429 responses) $waitSec = $retryAfterHeader ?? ($attempt * 2); // default: 2 s, 4 s, 6 s // Priority 2: Google's retryDelay inside the error body (e.g. {"retryDelay":"10s"}) - if ($body) { - $errData = json_decode($body, true); - foreach (($errData['error']['details'] ?? []) as $detail) { - if (!empty($detail['retryDelay'])) { - $parsed = intval(preg_replace('/\D/', '', $detail['retryDelay'])); - if ($parsed > 0) { $waitSec = min($parsed, 60); break; } - } + foreach (($errData['error']['details'] ?? []) as $detail) { + if (!empty($detail['retryDelay'])) { + $parsed = intval(preg_replace('/\D/', '', $detail['retryDelay'])); + if ($parsed > 0) { $waitSec = min($parsed, 60); break; } } } + // Filet de sécurité : jamais laisser le cumul des attentes dépasser le budget + if ((microtime(true) - $t0) + $waitSec >= $maxTotalElapsed) { + EverLog::warn('AI retry budget exhausted, failing fast', ['elapsed' => microtime(true) - $t0]); + break; + } + EverLog::warn('AI rate-limited, retrying', ['attempt' => $attempt, 'wait_s' => $waitSec, 'code' => $lastCode]); sleep($waitSec); }