| @@ -13,13 +13,17 @@ | ||
| 13 | 13 | declare(strict_types=1); |
| 14 | 14 | |
| 15 | 15 | namespace ThinkRank\AI; |
| 16 | 16 | |
| 17 | +use ThinkRank\AI\Traits\Request_Timeout; | |
| 18 | + | |
| 17 | 19 | // Prevent direct access |
| 18 | 20 | if (!defined('ABSPATH')) { |
| 19 | 21 | exit; |
| 20 | 22 | } |
| 21 | 23 | |
| 24 | +require_once __DIR__ . '/traits/trait-request-timeout.php'; | |
| 25 | + | |
| 22 | 26 | /** |
| 23 | 27 | * Gemini AI Client Class |
| 24 | 28 | * |
| 25 | 29 | * Provides interface to Google Gemini API for SEO optimization, |
| @@ -28,8 +32,11 @@ | ||
| 28 | 32 | * @since 1.0.0 |
| 29 | 33 | */ |
| 30 | 34 | class Gemini_Client { |
| 31 | 35 | |
| 36 | + use Request_Timeout; | |
| 37 | + | |
| 38 | + | |
| 32 | 39 | /** |
| 33 | 40 | * API key for Gemini |
| 34 | 41 | * |
| 35 | 42 | * @since 1.0.0 |
| @@ -409,8 +416,14 @@ | ||
| 409 | 416 | * @return array Response data |
| 410 | 417 | * @throws \Exception If request fails |
| 411 | 418 | */ |
| 412 | 419 | private function make_request(string $endpoint, array $data): array { |
| 420 | + // The user's daily ceiling and kill switch are enforced here, at the | |
| 421 | + // one place every outbound Gemini call passes through, so no feature | |
| 422 | + // path can bypass them by forgetting to ask first (#448). | |
| 423 | + Spend_Guard::guard(); | |
| 424 | + Spend_Guard::record(); | |
| 425 | + | |
| 413 | 426 | // Send the API key in the x-goog-api-key header rather than the URL |
| 414 | 427 | // query string, which is logged by servers, proxies and referrers. |
| 415 | 428 | $url = "https://generativelanguage.googleapis.com/v1beta/models/{$this->model}:{$endpoint}"; |
| 416 | 429 | |
| @@ -494,9 +507,14 @@ | ||
| 494 | 507 | |
| 495 | 508 | $is_transient = false; |
| 496 | 509 | $retry_after = 0; |
| 497 | 510 | if (is_wp_error($response)) { |
| 498 | - $is_transient = true; | |
| 511 | + // A client-side timeout means the work genuinely needs longer | |
| 512 | + // than the budget we allowed; re-running the identical prompt, | |
| 513 | + // model and budget just times out again and multiplies the | |
| 514 | + // wait (issue #288). Do not retry a timeout. Other WP_Error | |
| 515 | + // results — DNS, connection refused, TLS — stay retryable. | |
| 516 | + $is_transient = !$this->is_timeout_error($response); | |
| 499 | 517 | } else { |
| 500 | 518 | $status = wp_remote_retrieve_response_code($response); |
| 501 | 519 | if (429 === $status || $status >= 500) { |
| 502 | 520 | $is_transient = true; |