# thinkrank/1.0.2/includes/ai/class-gemini-client.php

ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console &amp; Local SEO, version 1.0.2. 717 lines.

- Page: https://pluginprobe.com/plugins/thinkrank/1.0.2/code/includes/ai/class-gemini-client.php
- Raw: https://pluginprobe.com/plugins/thinkrank/1.0.2/raw/includes/ai/class-gemini-client.php
- Modified: 2025-08-10T07:35:04+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/thinkrank/1.0.2/code/includes/ai/class-gemini-client.php#L10-L20`.

```php
<?php
/**
 * Google Gemini AI Client
 *
 * Handles communication with Google's Gemini API for AI-powered features.
 * Integrates with centralized prompt system for consistent prompts across providers.
 *
 * @package ThinkRank
 * @subpackage AI
 * @since 1.0.0
 */

declare(strict_types=1);

namespace ThinkRank\AI;

/**
 * Gemini AI Client Class
 *
 * Provides interface to Google Gemini API for SEO optimization,
 * content analysis, and other AI-powered features.
 *
 * @since 1.0.0
 */
class Gemini_Client {

    /**
     * API key for Gemini
     *
     * @since 1.0.0
     * @var string
     */
    private string $api_key;

    /**
     * Model to use for requests
     *
     * @since 1.0.0
     * @var string
     */
    private string $model;

    /**
     * Request timeout in seconds
     *
     * @since 1.0.0
     * @var int
     */
    private int $timeout;

    /**
     * Prompt Builder instance
     *
     * @since 1.0.0
     * @var Prompt_Builder|null
     */
    private ?Prompt_Builder $prompt_builder = null;

    /**
     * Constructor
     * 
     * @param string $api_key Gemini API key
     * @param string $model Default model to use
     * @param int $timeout Request timeout
     */
    public function __construct(string $api_key, string $model = 'gemini-2.5-flash', int $timeout = 30) {
        $this->api_key = $api_key;
        $this->model = $model;
        $this->timeout = $timeout;
    }

    /**
     * Get current model
     *
     * @since 1.0.0
     *
     * @return string Current model name
     */
    public function get_model(): string {
        return $this->model;
    }

    /**
     * Get Prompt Builder instance
     *
     * @since 1.0.0
     *
     * @return Prompt_Builder Prompt Builder instance
     */
    private function get_prompt_builder(): Prompt_Builder {
        if (!$this->prompt_builder) {
            // Ensure Prompt Builder is loaded
            if (!class_exists('ThinkRank\\AI\\Prompt_Builder')) {
                require_once THINKRANK_PLUGIN_DIR . 'includes/ai/class-prompt-builder.php';
            }
            $this->prompt_builder = new Prompt_Builder();
        }
        return $this->prompt_builder;
    }

    /**
     * Generate SEO metadata for content
     * 
     * @param string $content Content to optimize
     * @param array $options Generation options
     * @return array Generated metadata
     * @throws \Exception If generation fails
     */
    public function generate_seo_metadata(string $content, array $options = []): array {
        $target_keyword = $options['target_keyword'] ?? '';
        $content_type = $options['content_type'] ?? 'blog_post';
        $tone = $options['tone'] ?? 'professional';

        $prompt_builder = $this->get_prompt_builder();
        $prompt = $prompt_builder->build_seo_prompt($content, $target_keyword, $content_type, $tone, 'gemini');
        
        $response = $this->generate_completion($prompt, [
            'max_tokens' => 500,
            'temperature' => 0.3,
        ]);

        return $this->parse_seo_response($response);
    }

    /**
     * Analyze content for SEO optimization
     * 
     * @param string $content Content to analyze
     * @param array $metadata Existing metadata
     * @return array Analysis results
     * @throws \Exception If analysis fails
     */
    public function analyze_content(string $content, array $metadata = []): array {
        $prompt_builder = $this->get_prompt_builder();
        $prompt = $prompt_builder->build_analysis_prompt($content, $metadata, 'gemini');

        $response = $this->generate_completion($prompt, [
            'max_tokens' => 800,
            'temperature' => 0.3,
        ]);

        return $this->parse_analysis_response($response);
    }

    /**
     * Optimize site identity
     *
     * @since 1.0.0
     *
     * @param array $site_data Site data to optimize
     * @param array $options Optimization options
     * @return array Optimization results
     * @throws \Exception If optimization fails
     */
    public function optimize_site_identity(array $site_data, array $options = []): array {
        $business_type = $options['business_type'] ?? 'website';
        $target_audience = $options['target_audience'] ?? 'general';
        $tone = $options['tone'] ?? 'professional';

        $prompt_builder = $this->get_prompt_builder();
        $prompt = $prompt_builder->build_site_identity_prompt($site_data, $business_type, $target_audience, $tone, 'gemini');

        $response = $this->make_request('generateContent', [
            'contents' => [
                [
                    'parts' => [
                        ['text' => $prompt]
                    ]
                ]
            ],
            'systemInstruction' => [
                'parts' => [
                    ['text' => 'You are an expert SEO consultant specializing in site identity optimization. Provide actionable, specific recommendations in JSON format.']
                ]
            ],
            'generationConfig' => [
                'maxOutputTokens' => 2000, // Increased based on actual usage (1499 tokens used)
                'temperature' => 0.4,
            ]
        ]);

        return $this->parse_site_identity_response($response);
    }

    /**
     * Optimize homepage meta content
     *
     * @since 1.0.0
     *
     * @param array $content_data Meta content data
     * @param array $options Optimization options
     * @return array Optimization results
     * @throws \Exception If optimization fails
     */
    public function optimize_homepage_meta(array $content_data, array $options = []): array {
        $business_type = $options['business_type'] ?? 'website';
        $target_audience = $options['target_audience'] ?? 'general';
        $tone = $options['tone'] ?? 'professional';
        $context = $options['context'] ?? [];

        $prompt_builder = $this->get_prompt_builder();
        $prompt = $prompt_builder->build_homepage_meta_prompt($content_data, $business_type, $target_audience, $tone, $context, 'gemini');

        $response = $this->make_request('generateContent', [
            'contents' => [
                [
                    'parts' => [
                        ['text' => $prompt]
                    ]
                ]
            ],
            'systemInstruction' => [
                'parts' => [
                    ['text' => 'You are an expert SEO consultant specializing in homepage meta optimization. Provide actionable, specific recommendations in JSON format.']
                ]
            ],
            'generationConfig' => [
                'maxOutputTokens' => 1200, // Higher limit for Gemini homepage meta
                'temperature' => 0.4,
            ]
        ]);

        return $this->parse_homepage_meta_response($response);
    }

    /**
     * Optimize homepage hero content
     *
     * @since 1.0.0
     *
     * @param array $hero_data Hero content data
     * @param array $options Optimization options
     * @return array Optimization results
     * @throws \Exception If optimization fails
     */
    public function optimize_homepage_hero(array $hero_data, array $options = []): array {
        $business_type = $options['business_type'] ?? 'website';
        $target_audience = $options['target_audience'] ?? 'general';
        $tone = $options['tone'] ?? 'professional';
        $context = $options['context'] ?? [];

        $prompt_builder = $this->get_prompt_builder();
        $prompt = $prompt_builder->build_homepage_hero_prompt($hero_data, $business_type, $target_audience, $tone, $context, 'gemini');

        $response = $this->make_request('generateContent', [
            'contents' => [
                [
                    'parts' => [
                        ['text' => $prompt]
                    ]
                ]
            ],
            'systemInstruction' => [
                'parts' => [
                    ['text' => 'You are an expert SEO consultant specializing in homepage hero optimization. Provide actionable, specific recommendations in JSON format.']
                ]
            ],
            'generationConfig' => [
                'maxOutputTokens' => 1200, // Higher limit for Gemini homepage hero
                'temperature' => 0.4,
            ]
        ]);

        return $this->parse_homepage_hero_response($response);
    }

    /**
     * Optimize LLMs.txt content
     *
     * @since 1.0.0
     *
     * @param array $website_data Website data to optimize
     * @param array $options Optimization options
     * @return array Optimization results
     * @throws \Exception If optimization fails
     */
    public function optimize_llms_txt(array $website_data, array $options = []): array {
        // Use shared prompt builder for consistent prompts across all AI providers
        $prompt_builder = $this->get_prompt_builder();
        $prompt = $prompt_builder->build_llms_txt_prompt($website_data, $options, 'gemini');

        $response = $this->make_request('generateContent', [
            'contents' => [
                [
                    'parts' => [
                        ['text' => $prompt]
                    ]
                ]
            ],
            'generationConfig' => [
                'maxOutputTokens' => 2000, // Increased for Gemini 2.5 Flash compatibility
                'temperature' => 0.4,
            ]
        ]);

        return $this->parse_llms_txt_response($response);
    }

    /**
     * Generate completion using Gemini API
     * 
     * @param string $prompt Prompt to send
     * @param array $options Generation options
     * @return array API response
     * @throws \Exception If request fails
     */
    public function generate_completion(string $prompt, array $options = []): array {
        $max_tokens = $options['max_tokens'] ?? 1000;
        $temperature = $options['temperature'] ?? 0.7;

        return $this->make_request('generateContent', [
            'contents' => [
                [
                    'parts' => [
                        ['text' => $prompt]
                    ]
                ]
            ],
            'generationConfig' => [
                'maxOutputTokens' => $max_tokens,
                'temperature' => $temperature,
            ]
        ]);
    }

    /**
     * Make request to Gemini API
     * 
     * @param string $endpoint API endpoint
     * @param array $data Request data
     * @return array Response data
     * @throws \Exception If request fails
     */
    private function make_request(string $endpoint, array $data): array {
        $url = "https://generativelanguage.googleapis.com/v1beta/models/{$this->model}:{$endpoint}?key={$this->api_key}";



        $response = wp_remote_post($url, [
            'timeout' => $this->timeout,
            'headers' => [
                'Content-Type' => 'application/json',
            ],
            'body' => wp_json_encode($data),
        ]);

        if (is_wp_error($response)) {
            throw new \Exception('Gemini API request failed: ' . esc_html($response->get_error_message()));
        }

        $status_code = wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);

        if ($status_code !== 200) {
            $error_data = json_decode($body, true);
            $error_message = $error_data['error']['message'] ?? 'Unknown error';
            throw new \Exception('Gemini API error (' . esc_html($status_code) . '): ' . esc_html($error_message));
        }

        $decoded = json_decode($body, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('Invalid JSON response from Gemini API');
        }

        // Debug: Log token usage information
        if (isset($decoded['usageMetadata'])) {
            $usage = $decoded['usageMetadata'];
            $prompt_tokens = $usage['promptTokenCount'] ?? 0;
            $total_tokens = $usage['totalTokenCount'] ?? 0;
            $output_tokens = $total_tokens - $prompt_tokens;


        }

        return $decoded;
    }

    /**
     * Parse SEO response from Gemini
     *
     * @param array $response Gemini response
     * @return array Parsed metadata
     * @throws \Exception If parsing fails
     */
    private function parse_seo_response(array $response): array {


        if (!isset($response['candidates'][0]['content']['parts'][0]['text'])) {
            throw new \Exception('Invalid response format from Gemini');
        }

        $content = trim($response['candidates'][0]['content']['parts'][0]['text']);
        $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)

        // Extract JSON from response
        $json_start = strpos($content, '{');
        $json_end = strrpos($content, '}');

        if (false === $json_start || false === $json_end) {
            throw new \Exception('No valid JSON found in Gemini response');
        }

        $json_content = substr($content, $json_start, $json_end - $json_start + 1);
        $metadata = json_decode($json_content, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('Invalid JSON in Gemini response: ' . esc_html(json_last_error_msg()));
        }

        // Validate required fields
        $required_fields = ['title', 'description', 'focus_keyword'];
        foreach ($required_fields as $field) {
            if (!isset($metadata[$field])) {
                throw new \Exception('Missing required field: ' . esc_html($field));
            }
        }

        return [
            'title' => sanitize_text_field($metadata['title']),
            'description' => sanitize_textarea_field($metadata['description']),
            'focus_keyword' => sanitize_text_field($metadata['focus_keyword']),
            'suggestions' => array_map('sanitize_text_field', $metadata['suggestions'] ?? []),
            'generated_at' => current_time('mysql'),
            'provider' => 'gemini',
            'model' => $this->model,
            'tokens_used' => $response['usageMetadata']['totalTokenCount'] ?? 0,
            '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
        ];
    }

    /**
     * Parse analysis response from Gemini
     *
     * @param array $response Gemini response
     * @return array Parsed analysis
     * @throws \Exception If parsing fails
     */
    private function parse_analysis_response(array $response): array {
        if (!isset($response['candidates'][0]['content']['parts'][0]['text'])) {
            throw new \Exception('Invalid response format from Gemini');
        }

        $content = trim($response['candidates'][0]['content']['parts'][0]['text']);
        $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)

        // Extract JSON from response
        $json_start = strpos($content, '{');
        $json_end = strrpos($content, '}');

        if (false === $json_start || false === $json_end) {
            throw new \Exception('No valid JSON found in Gemini response');
        }

        $json_content = substr($content, $json_start, $json_end - $json_start + 1);
        $analysis = json_decode($json_content, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('Invalid JSON in Gemini response: ' . esc_html(json_last_error_msg()));
        }

        return [
            'seo_score' => absint($analysis['seo_score'] ?? 0),
            'content_analysis' => $analysis['content_analysis'] ?? [],
            'suggestions' => array_map('sanitize_text_field', $analysis['suggestions'] ?? []),
            'strengths' => array_map('sanitize_text_field', $analysis['strengths'] ?? []),
            'weaknesses' => array_map('sanitize_text_field', $analysis['weaknesses'] ?? []),
            'generated_at' => current_time('mysql'),
            'provider' => 'gemini',
            'model' => $this->model,
            'tokens_used' => $response['usageMetadata']['totalTokenCount'] ?? 0,
            '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
        ];
    }

    /**
     * Parse site identity optimization response
     *
     * @param array $response Gemini API response
     * @return array Parsed optimization data
     * @throws \Exception If parsing fails
     */
    private function parse_site_identity_response(array $response): array {
        if (!isset($response['candidates'][0]['content']['parts'][0]['text'])) {
            // Check if content was blocked by safety filters
            if (isset($response['candidates'][0]['finishReason']) && $response['candidates'][0]['finishReason'] === 'SAFETY') {
                throw new \Exception('Content was blocked by Gemini safety filters. Please try rephrasing your request.');
            }
            // Check if response was truncated due to token limit
            if (isset($response['candidates'][0]['finishReason']) && $response['candidates'][0]['finishReason'] === 'MAX_TOKENS') {
                throw new \Exception('Gemini response was truncated due to token limit. Please try a shorter request or increase token limit.');
            }
            // Check if there are no candidates
            if (!isset($response['candidates']) || empty($response['candidates'])) {
                throw new \Exception('No response candidates from Gemini. The request may have been filtered.');
            }
            // Check if content exists but parts are missing
            if (isset($response['candidates'][0]['content']) && !isset($response['candidates'][0]['content']['parts'])) {
                throw new \Exception('Gemini response missing content parts. The response may be incomplete.');
            }
            throw new \Exception('Invalid response format from Gemini');
        }

        $content = trim($response['candidates'][0]['content']['parts'][0]['text']);
        $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)

        // Extract JSON from response
        $json_start = strpos($content, '{');
        $json_end = strrpos($content, '}');

        if (false === $json_start || false === $json_end) {
            throw new \Exception('No valid JSON found in Gemini response');
        }

        $json_content = substr($content, $json_start, $json_end - $json_start + 1);
        $data = json_decode($json_content, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('Invalid JSON in Gemini response: ' . esc_html(json_last_error_msg()));
        }

        return [
            'optimized_data' => $data['optimized_data'] ?? [],
            'analysis' => sanitize_textarea_field($data['analysis'] ?? ''),
            'suggestions' => array_map('sanitize_text_field', $data['suggestions'] ?? []),
            'score' => absint($data['score'] ?? 0),
            'generated_at' => current_time('mysql'),
            'provider' => 'gemini',
            'model' => $this->model,
            'tokens_used' => $response['usageMetadata']['totalTokenCount'] ?? 0,
            '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
        ];
    }

    /**
     * Parse homepage meta optimization response
     *
     * @param array $response Gemini API response
     * @return array Parsed optimization data
     * @throws \Exception If parsing fails
     */
    private function parse_homepage_meta_response(array $response): array {
        if (!isset($response['candidates'][0]['content']['parts'][0]['text'])) {
            // Check if content was blocked by safety filters
            if (isset($response['candidates'][0]['finishReason']) && $response['candidates'][0]['finishReason'] === 'SAFETY') {
                throw new \Exception('Content was blocked by Gemini safety filters. Please try rephrasing your request.');
            }
            // Check if response was truncated due to token limit
            if (isset($response['candidates'][0]['finishReason']) && $response['candidates'][0]['finishReason'] === 'MAX_TOKENS') {
                throw new \Exception('Gemini response was truncated due to token limit. Please try a shorter request or increase token limit.');
            }
            // Check if there are no candidates
            if (!isset($response['candidates']) || empty($response['candidates'])) {
                throw new \Exception('No response candidates from Gemini. The request may have been filtered.');
            }
            // Check if content exists but parts are missing
            if (isset($response['candidates'][0]['content']) && !isset($response['candidates'][0]['content']['parts'])) {
                throw new \Exception('Gemini response missing content parts. The response may be incomplete.');
            }
            throw new \Exception('Invalid response format from Gemini');
        }

        $content = trim($response['candidates'][0]['content']['parts'][0]['text']);
        $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)

        // Extract JSON from response
        $json_start = strpos($content, '{');
        $json_end = strrpos($content, '}');

        if (false === $json_start || false === $json_end) {
            throw new \Exception('No valid JSON found in Gemini response');
        }

        $json_content = substr($content, $json_start, $json_end - $json_start + 1);
        $data = json_decode($json_content, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('Invalid JSON in Gemini response: ' . esc_html(json_last_error_msg()));
        }

        return [
            'optimized_data' => $data['optimized_data'] ?? [],
            'analysis' => sanitize_textarea_field($data['analysis'] ?? ''),
            'suggestions' => array_map('sanitize_text_field', $data['suggestions'] ?? []),
            'score' => absint($data['score'] ?? 0),
            'generated_at' => current_time('mysql'),
            'provider' => 'gemini',
            'model' => $this->model,
            'tokens_used' => $response['usageMetadata']['totalTokenCount'] ?? 0,
            '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
        ];
    }

    /**
     * Parse homepage hero optimization response
     *
     * @param array $response Gemini API response
     * @return array Parsed optimization data
     * @throws \Exception If parsing fails
     */
    private function parse_homepage_hero_response(array $response): array {
        if (!isset($response['candidates'][0]['content']['parts'][0]['text'])) {
            // Check if content was blocked by safety filters
            if (isset($response['candidates'][0]['finishReason']) && $response['candidates'][0]['finishReason'] === 'SAFETY') {
                throw new \Exception('Content was blocked by Gemini safety filters. Please try rephrasing your request.');
            }
            // Check if response was truncated due to token limit
            if (isset($response['candidates'][0]['finishReason']) && $response['candidates'][0]['finishReason'] === 'MAX_TOKENS') {
                throw new \Exception('Gemini response was truncated due to token limit. Please try a shorter request or increase token limit.');
            }
            // Check if there are no candidates
            if (!isset($response['candidates']) || empty($response['candidates'])) {
                throw new \Exception('No response candidates from Gemini. The request may have been filtered.');
            }
            // Check if content exists but parts are missing
            if (isset($response['candidates'][0]['content']) && !isset($response['candidates'][0]['content']['parts'])) {
                throw new \Exception('Gemini response missing content parts. The response may be incomplete.');
            }
            throw new \Exception('Invalid response format from Gemini');
        }

        $content = trim($response['candidates'][0]['content']['parts'][0]['text']);
        $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)

        // Extract JSON from response
        $json_start = strpos($content, '{');
        $json_end = strrpos($content, '}');

        if (false === $json_start || false === $json_end) {
            throw new \Exception('No valid JSON found in Gemini response');
        }

        $json_content = substr($content, $json_start, $json_end - $json_start + 1);
        $data = json_decode($json_content, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('Invalid JSON in Gemini response: ' . esc_html(json_last_error_msg()));
        }

        return [
            'optimized_data' => $data['optimized_data'] ?? [],
            'analysis' => sanitize_textarea_field($data['analysis'] ?? ''),
            'suggestions' => array_map('sanitize_text_field', $data['suggestions'] ?? []),
            'score' => absint($data['score'] ?? 0),
            'generated_at' => current_time('mysql'),
            'provider' => 'gemini',
            'model' => $this->model,
            'tokens_used' => $response['usageMetadata']['totalTokenCount'] ?? 0,
            '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
        ];
    }

    /**
     * Parse LLMs.txt optimization response
     *
     * @param array $response Gemini API response
     * @return array Parsed optimization data
     * @throws \Exception If parsing fails
     */
    private function parse_llms_txt_response(array $response): array {
        if (!isset($response['candidates'][0]['content']['parts'][0]['text'])) {
            // Check if content was blocked by safety filters
            if (isset($response['candidates'][0]['finishReason']) && $response['candidates'][0]['finishReason'] === 'SAFETY') {
                throw new \Exception('Content was blocked by Gemini safety filters. Please try rephrasing your request.');
            }
            // Check if response was truncated due to token limit
            if (isset($response['candidates'][0]['finishReason']) && $response['candidates'][0]['finishReason'] === 'MAX_TOKENS') {
                throw new \Exception('Gemini response was truncated due to token limit. Please try a shorter request or increase token limit.');
            }
            // Check if there are no candidates
            if (!isset($response['candidates']) || empty($response['candidates'])) {
                throw new \Exception('No response candidates from Gemini. The request may have been filtered.');
            }
            // Check if content exists but parts are missing
            if (isset($response['candidates'][0]['content']) && !isset($response['candidates'][0]['content']['parts'])) {
                throw new \Exception('Gemini response missing content parts. The response may be incomplete.');
            }
            throw new \Exception('Invalid response format from Gemini');
        }

        $content = trim($response['candidates'][0]['content']['parts'][0]['text']);
        $ai_text = $content; // Store the raw AI-generated text (Content Brief pattern)

        // Extract JSON from response
        $json_start = strpos($content, '{');
        $json_end = strrpos($content, '}');

        if (false === $json_start || false === $json_end) {
            throw new \Exception('No valid JSON found in Gemini response');
        }

        $json_content = substr($content, $json_start, $json_end - $json_start + 1);
        $data = json_decode($json_content, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception('Invalid JSON in Gemini response: ' . esc_html(json_last_error_msg()));
        }

        // Match the structure expected by AI Manager (same as OpenAI/Claude)
        return [
            'optimized_data' => [
                'site_name' => sanitize_text_field($data['optimized_data']['site_name'] ?? ''),
                'project_overview' => sanitize_textarea_field($data['optimized_data']['project_overview'] ?? ''),
                'key_features' => sanitize_textarea_field($data['optimized_data']['key_features'] ?? ''),
                'architecture' => sanitize_textarea_field($data['optimized_data']['architecture'] ?? ''),
                'development_guidelines' => sanitize_textarea_field($data['optimized_data']['development_guidelines'] ?? ''),
                'ai_context' => sanitize_textarea_field($data['optimized_data']['ai_context'] ?? ''),
            ],
            'suggestions' => array_map('sanitize_text_field', $data['suggestions'] ?? []),
            'generated_at' => current_time('mysql'),
            'provider' => 'gemini',
            'model' => $this->model,
            'tokens_used' => $response['usageMetadata']['totalTokenCount'] ?? 0,
            '_ai_text' => $ai_text, // Store the raw AI-generated text (Content Brief pattern)
        ];
    }
}

```
