PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.0
2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 All 51 releases
← All changes | includes/ai/class-gemini-client.php +42 -13 1.27.0 → 2.10.0 View file →
@@ -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
@@ -67,9 +74,9 @@
67 74 * @param string $api_key Gemini API key
68 75 * @param string $model Default model to use
69 76 * @param int $timeout Request timeout
70 77 */
71 - public function __construct(string $api_key, string $model = 'gemini-2.5-flash', int $timeout = 30) {
78 + public function __construct(string $api_key, string $model = \ThinkRank\Core\Settings::DEFAULT_GEMINI_MODEL, int $timeout = 30) {
72 79 $this->api_key = $api_key;
73 80 $this->model = $model;
74 81 $this->timeout = $timeout;
75 82 }
@@ -332,26 +339,27 @@
332 339 /**
333 340 * Build the generationConfig for a request, disabling "thinking" on
334 341 * Gemini 2.5 Flash models.
335 342 *
336 - * Gemini 2.5 Flash / Flash-Lite enable an internal "thinking" phase by
343 + * Gemini Flash models (2.5 and 3.x) enable an internal "thinking" phase by
337 344 * default, and those thoughts are billed against maxOutputTokens. On smaller
338 - * budgets — especially the free API tier used with the default
339 - * gemini-2.5-flash model — thinking can consume most of the budget, leaving
340 - * the visible answer truncated (finishReason=MAX_TOKENS) with incomplete
341 - * JSON. Downstream parsers then fail with "parsing failed". Setting
342 - * thinkingBudget to 0 disables thinking so the entire budget is spent on the
343 - * JSON answer.
345 + * budgets — especially the free API tier used with the default Flash model —
346 + * thinking can consume most of the budget, leaving the visible answer
347 + * truncated (finishReason=MAX_TOKENS) with incomplete JSON. Downstream
348 + * parsers then fail with "parsing failed". Setting thinkingBudget to 0
349 + * disables thinking so the entire budget is spent on the JSON answer.
344 350 *
345 - * Only 2.5 Flash models accept thinkingBudget=0; 2.5 Pro requires a minimum
351 + * Only Flash models accept thinkingBudget=0; Pro models require a minimum
346 352 * budget and pre-2.5 models reject thinkingConfig outright, so the override
347 - * is scoped to Flash models to avoid 400 errors.
353 + * is scoped to 2.5/3.x Flash models to avoid 400 errors. This deliberately
354 + * covers the current default (gemini-3.5-flash) as well as legacy
355 + * gemini-2.5-flash installs.
348 356 *
349 357 * @param array $config Caller-supplied generationConfig
350 358 * @return array generationConfig with thinking disabled where supported
351 359 */
352 360 private function build_generation_config(array $config): array {
353 - if (strpos($this->model, 'gemini-2.5-flash') === 0) {
361 + if (preg_match('/^gemini-(2\.5|3(?:\.\d+)?)-flash/', $this->model) === 1) {
354 362 $config['thinkingConfig'] = ['thinkingBudget' => 0];
355 363 }
356 364
357 365 if (isset($config['maxOutputTokens'])) {
@@ -408,8 +416,14 @@
408 416 * @return array Response data
409 417 * @throws \Exception If request fails
410 418 */
411 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 +
412 426 // Send the API key in the x-goog-api-key header rather than the URL
413 427 // query string, which is logged by servers, proxies and referrers.
414 428 $url = "https://generativelanguage.googleapis.com/v1beta/models/{$this->model}:{$endpoint}";
415 429
@@ -432,8 +446,18 @@
432 446
433 447 if ($status_code !== 200) {
434 448 $error_data = json_decode($body, true);
435 449 $error_message = $error_data['error']['message'] ?? 'Unknown error';
450 + // A 404 almost always means the configured model has been retired or
451 + // is not available to this API key. Surface an actionable message
452 + // pointing at the model setting instead of the provider's raw error.
453 + if ($status_code === 404) {
454 + throw new \Exception(sprintf(
455 + 'The selected Gemini model "%s" is unavailable (404). Choose a different model in ThinkRank → Settings → AI. (Provider message: %s)',
456 + esc_html($this->model),
457 + esc_html($error_message)
458 + ));
459 + }
436 460 throw new \Exception('Gemini API error (' . esc_html($status_code) . '): ' . esc_html($error_message));
437 461 }
438 462
439 463 $decoded = json_decode($body, true);
@@ -483,9 +507,14 @@
483 507
484 508 $is_transient = false;
485 509 $retry_after = 0;
486 510 if (is_wp_error($response)) {
487 - $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);
488 517 } else {
489 518 $status = wp_remote_retrieve_response_code($response);
490 519 if (429 === $status || $status >= 500) {
491 520 $is_transient = true;
@@ -847,9 +876,9 @@
847 876 return [
848 877 'optimized_data' => [
849 878 'site_name' => sanitize_text_field($data['optimized_data']['site_name'] ?? ''),
850 879 'project_overview' => sanitize_textarea_field($data['optimized_data']['project_overview'] ?? ''),
851 - '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'] ?? ''),
852 881 'architecture' => sanitize_textarea_field($data['optimized_data']['architecture'] ?? ''),
853 882 'development_guidelines' => sanitize_textarea_field($data['optimized_data']['development_guidelines'] ?? ''),
854 883 'ai_context' => sanitize_textarea_field($data['optimized_data']['ai_context'] ?? ''),
855 884 ],