| @@ -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; |
| @@ -858,9 +876,9 @@ | ||
| 858 | 876 | return [ |
| 859 | 877 | 'optimized_data' => [ |
| 860 | 878 | 'site_name' => sanitize_text_field($data['optimized_data']['site_name'] ?? ''), |
| 861 | 879 | 'project_overview' => sanitize_textarea_field($data['optimized_data']['project_overview'] ?? ''), |
| 862 | - 'key_features' => sanitize_textarea_field($data['optimized_data']['key_features'] ?? ''), | |
| 880 | + 'key_features' => \ThinkRank\SEO\LLMs_Txt_Manager::normalize_ai_key_features($data['optimized_data']['key_features'] ?? ''), | |
| 863 | 881 | 'architecture' => sanitize_textarea_field($data['optimized_data']['architecture'] ?? ''), |
| 864 | 882 | 'development_guidelines' => sanitize_textarea_field($data['optimized_data']['development_guidelines'] ?? ''), |
| 865 | 883 | 'ai_context' => sanitize_textarea_field($data['optimized_data']['ai_context'] ?? ''), |
| 866 | 884 | ], |