buildExtractionPrompt(); $payload = [ "contents" => [ [ "parts" => [ ["text" => $prompt . " If the image is not an invoice, is blank, or is completely unreadable, return ONLY: {\"error\": \"invalid_invoice\"}. DO NOT guess or invent data."], [ "inline_data" => [ "mime_type" => $mimeType, "data" => $base64Data ] ] ] ] ], "generationConfig" => [ "response_mime_type" => "application/json" ] ]; // Retry with exponential backoff for 503/429 errors for ($attempt = 1; $attempt <= self::$maxRetries; $attempt++) { $ch = curl_init(self::$baseUrl . "?key=" . $apiKey); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_TIMEOUT, 60); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($curlError) { error_log("AI Error: cURL failed (attempt $attempt): $curlError"); if ($attempt < self::$maxRetries) { $wait = pow(2, $attempt) + rand(1, 3); echo " Retrying in {$wait}s (cURL error)...\n"; sleep($wait); continue; } return null; } if ($httpCode === 200) { break; // Success } // Retry on 503 (overloaded) or 429 (rate limit) if (in_array($httpCode, [503, 429]) && $attempt < self::$maxRetries) { $wait = pow(2, $attempt) + rand(1, 3); echo " Gemini $httpCode — retrying in {$wait}s (attempt $attempt/" . self::$maxRetries . ")...\n"; sleep($wait); continue; } error_log("AI Error: Gemini API returned code $httpCode. Response: " . $response); return null; } if ($httpCode !== 200) { error_log("AI Error: All retries exhausted. Last code: $httpCode"); return null; } $result = json_decode($response, true); $textResponse = $result['candidates'][0]['content']['parts'][0]['text'] ?? null; if (!$textResponse) return null; $data = json_decode($textResponse, true); if (isset($data['error']) && $data['error'] === 'invalid_invoice') { return null; } return $data; } }