# thinkrank/1.0.1/includes/ai/class-manager.php

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

- Page: https://pluginprobe.com/plugins/thinkrank/1.0.1/code/includes/ai/class-manager.php
- Raw: https://pluginprobe.com/plugins/thinkrank/1.0.1/raw/includes/ai/class-manager.php
- Modified: 2025-08-10T10:57:26+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.1/code/includes/ai/class-manager.php#L10-L20`.

```php
<?php
/**
 * AI Manager Class
 *
 * Handles AI provider integration and management
 *
 * @package ThinkRank\AI
 * @since 1.0.0
 */

declare(strict_types=1);

namespace ThinkRank\AI;

use ThinkRank\Core\Settings;


// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * AI Manager Class
 *
 * Single Responsibility: Manage AI providers and requests
 *
 * @since 1.0.0
 */
class Manager {

    /**
     * Settings instance
     *
     * @var Settings
     */
    private Settings $settings;



    /**
     * Cache manager instance
     *
     * @var Cache_Manager
     */
    private Cache_Manager $cache;

    /**
     * Current AI client
     *
     * @var OpenAI_Client|Claude_Client|null
     */
    private $client = null;

    /**
     * Rate limiter
     *
     * @var array
     */
    private array $rate_limits = [];

    /**
     * Constructor
     *
     * @param Settings|null $settings Settings instance
     */
    public function __construct(?Settings $settings = null) {
        $this->settings = $settings ?? new Settings();
        $this->cache = new Cache_Manager((int) $this->settings->get('cache_duration', 3600));
    }

    /**
     * Initialize AI manager
     *
     * @return void
     */
    public function init(): void {
        // Initialize AI client based on settings
        add_action('init', [$this, 'initialize_client']);

        // Schedule cache cleanup
        add_action('thinkrank_daily_cleanup', [$this, 'cleanup_cache']);

        // Add AJAX handlers for AI requests
        add_action('wp_ajax_thinkrank_generate_metadata', [$this, 'ajax_generate_metadata']);
        add_action('wp_ajax_thinkrank_test_api_connection', [$this, 'ajax_test_connection']);
    }

    /**
     * Initialize AI client
     *
     * @return void
     */
    public function initialize_client(): void {
        $provider = $this->settings->get('ai_provider', 'openai');

        try {
            switch ($provider) {
                case 'openai':
                    $api_key = $this->settings->get('openai_api_key');
                    if ($api_key) {
                        $model = $this->settings->get('openai_model', 'gpt-5-nano');

                        $available_models = $this->get_available_providers()['openai']['models'];
                        if (!in_array($model, $available_models, true)) {
                            $model = 'gpt-5-nano';
                        }
                        // Use 120-second timeout for complex AI operations
                        $timeout = 120;
                        $this->client = new OpenAI_Client($api_key, $model, $timeout);

                        // OpenAI client created successfully
                    }
                    break;

                case 'claude':
                    $api_key = $this->settings->get('claude_api_key');
                    if ($api_key) {
                        $model = $this->settings->get('claude_model', 'claude-3-7-sonnet-latest');

                        $available_models = $this->get_available_providers()['claude']['models'];
                        if (!in_array($model, $available_models, true)) {
                            $model = 'claude-3-7-sonnet-latest';
                        }
                        // Use 120-second timeout for complex AI operations
                        $timeout = 120;
                        $this->client = new Claude_Client($api_key, $model, $timeout);

                        // Claude client created successfully
                    }
                    break;

                case 'gemini':
                    $api_key = $this->settings->get('gemini_api_key');
                    if ($api_key) {
                        $model = $this->settings->get('gemini_model', 'gemini-2.5-flash');

                        $available_models = $this->get_available_providers()['gemini']['models'];
                        if (!in_array($model, $available_models, true)) {
                            $model = 'gemini-2.5-flash';
                        }
                        // Use 120-second timeout for complex AI operations
                        $timeout = 120;
                        $this->client = new Gemini_Client($api_key, $model, $timeout);
                    }
                    break;

                default:
                    throw new \Exception("Unsupported AI provider: {$provider}");
            }
        } catch (\Exception $e) {
            // AI client initialization failed, will be handled later
        }
    }

    /**
     * Force re-initialization of client (useful after settings change)
     *
     * @return void
     */
    public function reinitialize_client(): void {
        $this->client = null;
        $this->initialize_client();
    }

    /**
     * Get the AI client instance
     *
     * @return OpenAI_Client|Claude_Client|null AI client instance
     * @throws \Exception If client cannot be initialized
     */
    public function get_client() {
        // Initialize client if not already done
        if (!$this->client) {
            $this->initialize_client();
        }

        // If still not available, throw error
        if (!$this->client) {
            throw new \Exception('AI client not initialized. Please configure your API key.');
        }

        return $this->client;
    }

    /**
     * Generate SEO metadata for content
     *
     * @param string $content Content to analyze
     * @param array $options Generation options
     * @return array Generated metadata
     * @throws \Exception If generation fails
     */
    public function generate_seo_metadata(string $content, array $options = []): array {
        // Check if client is available, try to initialize if not
        if (!$this->client) {
            $this->initialize_client();

            // If still not available, throw error
            if (!$this->client) {
                throw new \Exception('AI client not initialized. Please configure your API key.');
            }
        }

        // Check rate limits
        if (!$this->check_rate_limit()) {
            throw new \Exception('Rate limit exceeded. Please try again later.');
        }

        // Get current user for logging
        $user_id = get_current_user_id();

        // Generate cache key
        $cache_key = $this->cache->generate_content_key($content, $options);

        // Check cache first
        $cached_result = $this->cache->get($cache_key);
        if ($cached_result !== null) {
            return $cached_result['data'];
        }

        try {
            // Generate metadata using AI
            $metadata = $this->client->generate_seo_metadata($content, $options);

            // Ensure user has configured their API key
            $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));

            if (!$user_has_api_key) {
                throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
            }

            // Cache the result
            $this->cache->set($cache_key, $metadata);

            // Log usage with actual model information and raw AI text (Content Brief pattern)
            $actual_model = $this->client ? $this->client->get_model() : null;
            $ai_text = $metadata['_ai_text'] ?? null;
            $this->log_ai_usage($user_id, 'SEO Metadata', $metadata['tokens_used'] ?? 0, $actual_model, $ai_text);

            // Remove AI text from returned data to keep it clean
            unset($metadata['_ai_text']);

            return $metadata;

        } catch (\Exception $e) {
            throw $e;
        }
    }

    /**
     * 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 {
        if (!$this->client) {
            throw new \Exception('AI client not initialized');
        }

        $user_id = get_current_user_id();

        // Ensure user has configured their API key
        $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));

        if (!$user_has_api_key) {
            throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
        }

        // Check rate limits
        if (!$this->check_rate_limit($user_id, 'content_analysis')) {
            throw new \Exception('Rate limit exceeded. Please try again later.');
        }

        // Check cache first
        $cache_key = 'content_analysis_' . md5($content . serialize($metadata));
        $cached_result = $this->cache->get($cache_key);
        if ($cached_result) {
            return $cached_result;
        }

        try {
            // Analyze content using AI
            $analysis = $this->client->analyze_content($content, $metadata);

            // Cache the result
            $this->cache->set($cache_key, $analysis);

            // Log usage with actual model information and raw AI text (Content Brief pattern)
            $actual_model = $this->client ? $this->client->get_model() : null;
            $ai_text = $analysis['_ai_text'] ?? null;
            $this->log_ai_usage($user_id, 'Content Analysis', $analysis['tokens_used'] ?? 0, $actual_model, $ai_text);

            // Remove AI text from returned data to keep it clean
            unset($analysis['_ai_text']);

            return $analysis;

        } catch (\Exception $e) {
            throw $e;
        }
    }

    /**
     * Test API connection
     *
     * @return array Test result
     */
    public function test_api_connection(): array {
        if (!$this->client) {
            return [
                'success' => false,
                'message' => 'AI client not initialized. Please configure your API key.',
            ];
        }

        try {
            $success = $this->client->test_connection();

            return [
                'success' => $success,
                'message' => $success
                    ? 'API connection successful!'
                    : 'API connection failed. Please check your API key.',
            ];

        } catch (\Exception $e) {
            return [
                'success' => false,
                'message' => 'Connection test failed: ' . $e->getMessage(),
            ];
        }
    }

    /**
     * Optimize site identity using AI
     *
     * @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 {
        // Validate input
        if (empty($site_data)) {
            throw new \Exception('Site data cannot be empty');
        }

        $user_id = get_current_user_id();

        // Ensure user has configured their API key
        $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));

        if (!$user_has_api_key) {
            throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
        }

        // Generate cache key using existing pattern
        $cache_key = 'site_identity_' . md5(serialize($site_data) . serialize($options)) . '_' . $user_id;

        // Check existing cache infrastructure
        $cached_result = $this->cache->get($cache_key);
        if ($cached_result !== null && !empty($cached_result['optimized_data'])) {
            return $cached_result;
        }

        // Check rate limiting
        if (!$this->check_rate_limit()) {
            throw new \Exception('Rate limit exceeded for AI optimization requests.');
        }

        // Get AI client
        $client = $this->get_client();

        if (!$client) {
            throw new \Exception('AI client not initialized. Please check your API key configuration.');
        }

        // Perform AI optimization
        $optimization_results = $client->optimize_site_identity($site_data, $options);

        // Validate that we got meaningful results
        if (empty($optimization_results) || empty($optimization_results['optimized_data'])) {
            throw new \Exception('AI optimization returned empty results. Please try again.');
        }

        // Add metadata
        $optimization_results['ai_model'] = $client->get_model();
        $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai');
        $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s');
        $optimization_results['user_id'] = $user_id;

        // Cache the results (24 hours)
        $this->cache->set($cache_key, $optimization_results, 86400);

        // Record usage with actual model information and raw AI text (Content Brief pattern)
        $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null);
        $ai_text = $optimization_results['_ai_text'] ?? null;
        $this->log_ai_usage($user_id, 'Site Identity Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text);

        // Remove AI text from returned data to keep it clean
        unset($optimization_results['_ai_text']);

        return $optimization_results;
    }

    /**
     * Optimize LLMs.txt content using AI
     *
     * @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 {
        // Validate input
        if (empty($website_data)) {
            throw new \Exception('Website data cannot be empty');
        }

        $user_id = get_current_user_id();

        // Ensure user has configured their API key
        $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));

        if (!$user_has_api_key) {
            throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
        }

        // Generate cache key
        $cache_key = 'llms_txt_' . md5(serialize($website_data) . serialize($options)) . '_' . $user_id;

        // Check cache first
        $cached_result = $this->cache->get($cache_key);
        if ($cached_result !== null && !empty($cached_result['optimized_data'])) {
            return $cached_result;
        }

        // Check rate limiting
        if (!$this->check_rate_limit()) {
            throw new \Exception('Rate limit exceeded for AI optimization requests.');
        }

        // Get AI client
        $client = $this->get_client();

        if (!$client) {
            throw new \Exception('AI client not initialized. Please check your API key configuration.');
        }

        // Perform AI optimization
        $optimization_results = $client->optimize_llms_txt($website_data, $options);

        // Validate that we got meaningful results
        if (empty($optimization_results) || empty($optimization_results['optimized_data'])) {
            throw new \Exception('AI optimization returned empty results. Please try again.');
        }

        // Add metadata
        $optimization_results['ai_model'] = $client->get_model();
        $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai');
        $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s');
        $optimization_results['user_id'] = $user_id;

        // Cache the results (24 hours)
        $this->cache->set($cache_key, $optimization_results, 86400);

        // Record usage with actual model information and raw AI text (Content Brief pattern)
        $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null);
        $ai_text = $optimization_results['_ai_text'] ?? null;
        $this->log_ai_usage($user_id, 'LLMs.txt Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text);

        // Remove AI text from returned data to keep it clean
        unset($optimization_results['_ai_text']);

        return $optimization_results;
    }

    /**
     * Get available AI providers
     *
     * @return array Available providers
     */
    public function get_available_providers(): array {
        return [
            'openai' => [
                'name' => 'OpenAI',
                'description' => 'GPT‑5 series and GPT‑4o',
                'models' => ['gpt-5-nano', 'gpt-5-mini', 'gpt-5', 'gpt-4o'],
                'requires_key' => true,
            ],
            'claude' => [
                'name' => 'Claude (Anthropic)',
                'description' => 'Claude 4 and 3.7 models',
                'models' => ['claude-sonnet-4-0', 'claude-opus-4-0', 'claude-3-7-sonnet-latest', 'claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest'],
                'requires_key' => true,
            ],
            'gemini' => [
                'name' => 'Google Gemini',
                'description' => 'Gemini 2.5 and 2.0 models',
                'models' => ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro', 'gemini-2.0-flash', 'gemini-1.5-flash'],
                'requires_key' => true,
            ],
        ];
    }

    /**
     * Get current provider status
     *
     * @return array Provider status
     */
    public function get_provider_status(): array {
        $provider = $this->settings->get('ai_provider', 'openai');
        $api_key = $this->settings->get($provider . '_api_key');

        return [
            'provider' => $provider,
            'configured' => !empty($api_key),
            'connected' => $this->client !== null,
        ];
    }

    /**
     * AJAX handler for generating metadata
     *
     * @return void
     */
    public function ajax_generate_metadata(): void {
        // Verify nonce
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce'] ?? ''));
        if (!wp_verify_nonce($nonce, 'thinkrank_ai_nonce')) {
            wp_die('Security check failed');
        }

        // Check permissions
        if (!current_user_can('edit_posts')) {
            wp_die('Insufficient permissions');
        }

        $content = sanitize_textarea_field(wp_unslash($_POST['content'] ?? ''));
        $options = [
            'target_keyword' => sanitize_text_field(wp_unslash($_POST['target_keyword'] ?? '')),
            'content_type' => sanitize_text_field(wp_unslash($_POST['content_type'] ?? 'blog_post')),
            'tone' => sanitize_text_field(wp_unslash($_POST['tone'] ?? 'professional')),
        ];

        try {
            $metadata = $this->generate_seo_metadata($content, $options);

            wp_send_json_success([
                'metadata' => $metadata,
                'message' => __('SEO metadata generated successfully!', 'thinkrank'),
            ]);

        } catch (\Exception $e) {
            wp_send_json_error([
                'message' => $e->getMessage(),
            ]);
        }
    }

    /**
     * AJAX handler for testing API connection
     *
     * @return void
     */
    public function ajax_test_connection(): void {
        // Verify nonce
        $nonce = sanitize_text_field(wp_unslash($_POST['nonce'] ?? ''));
        if (!wp_verify_nonce($nonce, 'thinkrank_ai_nonce')) {
            wp_die('Security check failed');
        }

        // Check permissions
        if (!current_user_can('manage_options')) {
            wp_die('Insufficient permissions');
        }

        $result = $this->test_api_connection();

        if ($result['success']) {
            wp_send_json_success($result);
        } else {
            wp_send_json_error($result);
        }
    }

    /**
     * Check rate limits
     *
     * @return bool True if within limits
     */
    private function check_rate_limit(): bool {
        $user_id = get_current_user_id();
        $max_requests = $this->settings->get('max_requests_per_minute', 10);
        $current_time = time();
        $window_start = $current_time - 60; // 1 minute window

        // Clean old entries
        $this->rate_limits = array_filter(
            $this->rate_limits,
            function($timestamp) use ($window_start) {
                return $timestamp > $window_start;
            }
        );

        // Count requests for this user
        $user_requests = array_filter(
            $this->rate_limits,
            function($timestamp, $key) use ($user_id) {
                return strpos($key, "user_{$user_id}_") === 0;
            },
            ARRAY_FILTER_USE_BOTH
        );

        if (count($user_requests) >= $max_requests) {
            return false;
        }

        // Add current request
        $this->rate_limits["user_{$user_id}_{$current_time}"] = $current_time;

        return true;
    }



    /**
     * Log AI usage with actual model information
     *
     * @param int $user_id User ID
     * @param string $action Action performed
     * @param int $tokens_used Tokens consumed
     * @param string|null $actual_model Actual model used (from AI response)
     * @param string|null $raw_response Raw AI response for debugging
     * @return void
     */
    private function log_ai_usage(int $user_id, string $action, int $tokens_used, ?string $actual_model = null, ?string $raw_response = null): void {
        global $wpdb;

        $table_name = $wpdb->prefix . 'thinkrank_ai_usage';

        // Prepare metadata with actual model information and raw response
        $metadata = [];
        if ($actual_model) {
            $metadata['actual_model'] = $actual_model;
        }
        if ($raw_response) {
            $metadata['raw_response'] = $raw_response;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- AI usage logging requires direct database access
        $wpdb->insert(
            $table_name,
            [
                'user_id' => $user_id,
                'action' => $action,
                'tokens_used' => $tokens_used,
                'provider' => $this->settings->get('ai_provider', 'openai'),
                'metadata' => !empty($metadata) ? wp_json_encode($metadata) : null,
                'created_at' => current_time('mysql'),
            ],
            ['%d', '%s', '%d', '%s', '%s', '%s']
        );
    }

    /**
     * Cleanup expired cache entries
     *
     * @return void
     */
    public function cleanup_cache(): void {
        $this->cache->clean_expired();
    }

    /**
     * Optimize homepage meta content using AI (copying Site Identity pattern exactly)
     *
     * @since 1.0.0
     *
     * @param array $content_data Meta content data to optimize
     * @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 {
        // Validate input
        if (empty($content_data)) {
            throw new \Exception('Content data cannot be empty');
        }

        $user_id = get_current_user_id();

        // Ensure user has configured their API key (copying Site Identity pattern)
        $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));

        if (!$user_has_api_key) {
            throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
        }

        // Generate cache key using existing pattern
        $cache_key = 'homepage_meta_' . md5(serialize($content_data) . serialize($options)) . '_' . $user_id;

        // Check existing cache infrastructure
        $cached_result = $this->cache->get($cache_key);
        if ($cached_result !== null && !empty($cached_result['optimized_data'])) {
            return $cached_result;
        }

        // Check rate limiting
        if (!$this->check_rate_limit()) {
            throw new \Exception('Rate limit exceeded for AI optimization requests.');
        }

        // Get AI client
        $client = $this->get_client();

        if (!$client) {
            throw new \Exception('AI client not initialized. Please check your API key configuration.');
        }

        // Perform AI optimization
        $optimization_results = $client->optimize_homepage_meta($content_data, $options);

        // Validate that we got meaningful results
        if (empty($optimization_results) || empty($optimization_results['optimized_data'])) {
            throw new \Exception('AI optimization returned empty results. Please try again.');
        }

        // Add metadata
        $optimization_results['ai_model'] = $client->get_model();
        $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai');
        $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s');
        $optimization_results['user_id'] = $user_id;

        // Cache the results (24 hours)
        $this->cache->set($cache_key, $optimization_results, 86400);

        // Record usage with actual model information and raw AI text (Content Brief pattern)
        $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null);
        $ai_text = $optimization_results['_ai_text'] ?? null;
        $this->log_ai_usage($user_id, 'Homepage Meta Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text);

        // Remove AI text from returned data to keep it clean
        unset($optimization_results['_ai_text']);

        return $optimization_results;
    }

    /**
     * Optimize homepage hero content using AI (copying Site Identity pattern exactly)
     *
     * @since 1.0.0
     *
     * @param array $hero_data Hero content data to optimize
     * @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 {
        // Validate input
        if (empty($hero_data)) {
            throw new \Exception('Hero data cannot be empty');
        }

        $user_id = get_current_user_id();

        // Ensure user has configured their API key (copying Site Identity pattern)
        $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key'));

        if (!$user_has_api_key) {
            throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.');
        }

        // Generate cache key using existing pattern
        $cache_key = 'homepage_hero_' . md5(serialize($hero_data) . serialize($options)) . '_' . $user_id;

        // Check existing cache infrastructure
        $cached_result = $this->cache->get($cache_key);
        if ($cached_result !== null && !empty($cached_result['optimized_data'])) {
            return $cached_result;
        }

        // Check rate limiting
        if (!$this->check_rate_limit()) {
            throw new \Exception('Rate limit exceeded for AI optimization requests.');
        }

        // Get AI client
        $client = $this->get_client();

        if (!$client) {
            throw new \Exception('AI client not initialized. Please check your API key configuration.');
        }

        // Perform AI optimization
        $optimization_results = $client->optimize_homepage_hero($hero_data, $options);

        // Validate that we got meaningful results
        if (empty($optimization_results) || empty($optimization_results['optimized_data'])) {
            throw new \Exception('AI optimization returned empty results. Please try again.');
        }

        // Add metadata
        $optimization_results['ai_model'] = $client->get_model();
        $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai');
        $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s');
        $optimization_results['user_id'] = $user_id;

        // Cache the results (24 hours)
        $this->cache->set($cache_key, $optimization_results, 86400);

        // Record usage with actual model information and raw AI text (Content Brief pattern)
        $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null);
        $ai_text = $optimization_results['_ai_text'] ?? null;
        $this->log_ai_usage($user_id, 'Homepage Hero Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text);

        // Remove AI text from returned data to keep it clean
        unset($optimization_results['_ai_text']);

        return $optimization_results;
    }
}

```
