PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.0
2.14.3 2.14.2 2.14.1 2.14.0 2.13.0 2.12.0 2.11.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 All 58 releases
← All changes | includes/ai/class-gemini-client.php +35 -12 1.27.0 → 2.7.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'])) {
@@ -432,8 +440,18 @@
432 440
433 441 if ($status_code !== 200) {
434 442 $error_data = json_decode($body, true);
435 443 $error_message = $error_data['error']['message'] ?? 'Unknown error';
444 + // A 404 almost always means the configured model has been retired or
445 + // is not available to this API key. Surface an actionable message
446 + // pointing at the model setting instead of the provider's raw error.
447 + if ($status_code === 404) {
448 + throw new \Exception(sprintf(
449 + 'The selected Gemini model "%s" is unavailable (404). Choose a different model in ThinkRank → Settings → AI. (Provider message: %s)',
450 + esc_html($this->model),
451 + esc_html($error_message)
452 + ));
453 + }
436 454 throw new \Exception('Gemini API error (' . esc_html($status_code) . '): ' . esc_html($error_message));
437 455 }
438 456
439 457 $decoded = json_decode($body, true);
@@ -483,9 +501,14 @@
483 501
484 502 $is_transient = false;
485 503 $retry_after = 0;
486 504 if (is_wp_error($response)) {
487 - $is_transient = true;
505 + // A client-side timeout means the work genuinely needs longer
506 + // than the budget we allowed; re-running the identical prompt,
507 + // model and budget just times out again and multiplies the
508 + // wait (issue #288). Do not retry a timeout. Other WP_Error
509 + // results — DNS, connection refused, TLS — stay retryable.
510 + $is_transient = !$this->is_timeout_error($response);
488 511 } else {
489 512 $status = wp_remote_retrieve_response_code($response);
490 513 if (429 === $status || $status >= 500) {
491 514 $is_transient = true;