table_name = $wpdb->prefix . 'aieo_ai_models'; add_action('wp_ajax_aieo_refresh_ai_models', array($this, 'ajax_refresh_models')); add_action('wp_ajax_aieo_validate_api_keys', array($this, 'ajax_validate_api_keys')); add_action('wp_ajax_aieo_chat_model_test', array($this, 'ajax_model_test')); add_action('wp_ajax_aieo_chat_model_detect', array($this, 'ajax_model_detect')); add_action('admin_init', array($this, 'maybe_create_table')); if (!wp_next_scheduled('aieo_monthly_model_refresh')) { wp_schedule_event(time(), 'monthly', 'aieo_monthly_model_refresh'); } add_action('aieo_monthly_model_refresh', array($this, 'cron_refresh_models')); } public function cron_refresh_models() { if ('' === (string) get_option('aieo_rooster_api_key', '')) { return; } return $this->refresh_all_models(); } public function maybe_create_table() { global $wpdb; $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$this->table_name}'"); if ($table_exists) { return; } $this->create_table(); } public function create_table() { global $wpdb; $charset_collate = aieo_get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$this->table_name} ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, provider varchar(50) NOT NULL, model_id varchar(100) NOT NULL, model_name varchar(255) NOT NULL, model_type varchar(50) NOT NULL DEFAULT 'chat', description text DEFAULT NULL, max_tokens int(11) DEFAULT NULL, dimensions int(11) DEFAULT NULL, is_recommended tinyint(1) DEFAULT 0, is_active tinyint(1) DEFAULT 1, sort_order int(11) DEFAULT 0, metadata longtext DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY provider_model (provider, model_id), KEY model_type (model_type), KEY is_active (is_active), KEY sort_order (sort_order) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); function_exists('aieo_dbdelta') ? aieo_dbdelta($sql) : dbDelta($sql); $count = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name}"); if ($count == 0) { $this->insert_fallback_models(); } } private function insert_fallback_models() { $this->insert_model(self::PROVIDER_ANTHROPIC, 'claude-opus-4-5-20251101', 'Claude Opus 4.5', self::TYPE_CHAT, 'Most intelligent model - hybrid reasoning', 16384, null, true, 10); $this->insert_model(self::PROVIDER_ANTHROPIC, 'claude-sonnet-4-5-20250929', 'Claude Sonnet 4.5', self::TYPE_CHAT, 'Balanced performance and speed', 16384, null, false, 20); $this->insert_model(self::PROVIDER_ANTHROPIC, 'claude-haiku-4-5-20251001', 'Claude Haiku 4.5', self::TYPE_CHAT, 'Fastest and most economical', 8192, null, false, 30); $this->insert_model(self::PROVIDER_ANTHROPIC, 'claude-opus-4-1-20250805', 'Claude Opus 4.1', self::TYPE_CHAT, 'Advanced reasoning capabilities', 8192, null, false, 40); $this->insert_model(self::PROVIDER_ANTHROPIC, 'claude-sonnet-4-20250514', 'Claude Sonnet 4', self::TYPE_CHAT, 'Balanced Claude 4 model', 8192, null, false, 50); $this->insert_model(self::PROVIDER_ANTHROPIC, 'claude-3-5-sonnet-20241022', 'Claude 3.5 Sonnet', self::TYPE_CHAT, 'Previous generation model', 8192, null, false, 60); $this->insert_model(self::PROVIDER_ANTHROPIC, 'claude-3-5-haiku-20241022', 'Claude 3.5 Haiku', self::TYPE_CHAT, 'Fast and economical', 8192, null, false, 70); $this->insert_model(self::PROVIDER_OPENAI, 'gpt-4o', 'GPT-4o', self::TYPE_CHAT, 'Most capable multimodal model', 16384, null, true, 10); $this->insert_model(self::PROVIDER_OPENAI, 'gpt-4o-mini', 'GPT-4o Mini', self::TYPE_CHAT, 'Fast and economical', 16384, null, false, 20); $this->insert_model(self::PROVIDER_OPENAI, 'gpt-4-turbo', 'GPT-4 Turbo', self::TYPE_CHAT, 'GPT-4 Turbo with vision', 4096, null, false, 30); $this->insert_model(self::PROVIDER_OPENAI, 'o1', 'O1', self::TYPE_CHAT, 'Advanced reasoning model', 32768, null, false, 40); $this->insert_model(self::PROVIDER_OPENAI, 'o1-mini', 'O1 Mini', self::TYPE_CHAT, 'Reasoning model - economical', 16384, null, false, 50); $this->insert_model(self::PROVIDER_OPENAI, 'o3-mini', 'O3 Mini', self::TYPE_CHAT, 'Latest reasoning model', 16384, null, false, 60); $this->insert_model(self::PROVIDER_OPENAI, 'gpt-3.5-turbo', 'GPT-3.5 Turbo', self::TYPE_CHAT, 'Budget-friendly option', 4096, null, false, 70); $this->insert_model(self::PROVIDER_OPENAI, 'text-embedding-3-large', 'Text Embedding 3 Large', self::TYPE_EMBEDDING, 'Best quality embeddings', null, 3072, true, 10); $this->insert_model(self::PROVIDER_OPENAI, 'text-embedding-3-small', 'Text Embedding 3 Small', self::TYPE_EMBEDDING, 'Efficient embeddings', null, 1536, false, 20); $this->insert_model(self::PROVIDER_OPENAI, 'text-embedding-ada-002', 'Text Embedding Ada 002', self::TYPE_EMBEDDING, 'Legacy embeddings', null, 1536, false, 30); $sort = 10; foreach (self::voyage_catalogue() as $row) { list($id, $name, $description, $dimensions, $recommended, $status) = $row; if ('retired' === $status) { continue; } $this->insert_model(self::PROVIDER_VOYAGE, $id, $name, self::TYPE_EMBEDDING, $description, null, $dimensions, $recommended, $sort); $sort += 10; } } public static function voyage_catalogue() { return array( array('voyage-4-large', 'Voyage 4 Large', 'Best retrieval quality, multilingual', 1024, false, 'current'), array('voyage-4', 'Voyage 4', 'General purpose, multilingual', 1024, true, 'current'), array('voyage-4-lite', 'Voyage 4 Lite', 'Lowest cost and latency', 1024, false, 'current'), array('voyage-code-4', 'Voyage Code 4', 'Code and technical documentation', 1024, false, 'current'), array('voyage-finance-2', 'Voyage Finance 2', 'Financial text', 1024, false, 'current'), array('voyage-law-2', 'Voyage Law 2', 'Legal text', 1024, false, 'current'), array('voyage-3.5', 'Voyage 3.5', 'Previous generation', 1024, false, 'legacy'), array('voyage-3.5-lite', 'Voyage 3.5 Lite', 'Previous generation, low cost', 1024, false, 'legacy'), array('voyage-3-large', 'Voyage 3 Large', 'Previous generation', 1024, false, 'legacy'), array('voyage-3', 'Voyage 3', 'Previous generation', 1024, false, 'legacy'), array('voyage-3-lite', 'Voyage 3 Lite', 'Previous generation, low cost', 512, false, 'legacy'), array('voyage-code-3', 'Voyage Code 3', 'Previous generation, code', 1024, false, 'legacy'), array('voyage-multilingual-2', 'Voyage Multilingual 2', 'Previous generation, multilingual', 1024, false, 'legacy'), array('voyage-large-2', 'Voyage Large 2', 'Legacy model', 1536, false, 'legacy'), array('voyage-2', 'Voyage 2', 'Legacy model', 1024, false, 'legacy'), array('voyage-code-2', 'Voyage Code 2', 'No longer served by Voyage', 1536, false, 'retired'), array('voyage-4-nano', 'Voyage 4 Nano', 'Open-weight only, not served through the API', 1024, false, 'retired'), ); } const VOYAGE_PHANTOMS = array('voyage-3.5-large'); private static function voyage_catalogue_i18n() { return array( __('Best retrieval quality, multilingual', 'ai-eshop-optimizer'), __('General purpose, multilingual', 'ai-eshop-optimizer'), __('Lowest cost and latency', 'ai-eshop-optimizer'), __('Code and technical documentation', 'ai-eshop-optimizer'), __('Financial text', 'ai-eshop-optimizer'), __('Legal text', 'ai-eshop-optimizer'), __('Previous generation', 'ai-eshop-optimizer'), __('Previous generation, low cost', 'ai-eshop-optimizer'), __('Previous generation, code', 'ai-eshop-optimizer'), __('Previous generation, multilingual', 'ai-eshop-optimizer'), __('Legacy model', 'ai-eshop-optimizer'), __('No longer served by Voyage', 'ai-eshop-optimizer'), __('Open-weight only, not served through the API', 'ai-eshop-optimizer'), ); } public static function describe($model) { $text = is_object($model) ? (string) ($model->description ?? '') : (string) $model; if ('' === $text) { return ''; } return __($text, 'ai-eshop-optimizer'); } private function insert_model($provider, $model_id, $name, $type, $description = '', $max_tokens = null, $dimensions = null, $recommended = false, $sort = 0) { global $wpdb; $wpdb->replace( $this->table_name, array( 'provider' => $provider, 'model_id' => $model_id, 'model_name' => $name, 'model_type' => $type, 'description' => $description, 'max_tokens' => $max_tokens, 'dimensions' => $dimensions, 'is_recommended' => $recommended ? 1 : 0, 'is_active' => 1, 'sort_order' => $sort ), array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d', '%d') ); } private function set_model_active($provider, $model_id, $active) { global $wpdb; $wpdb->update( $this->table_name, array('is_active' => $active ? 1 : 0), array('provider' => $provider, 'model_id' => $model_id), array('%d'), array('%s', '%s') ); } public function get_models($provider = null, $type = null, $active_only = true) { global $wpdb; $where = array(); $values = array(); if ($provider !== null) { $where[] = 'provider = %s'; $values[] = $provider; } if ($type !== null) { $where[] = 'model_type = %s'; $values[] = $type; } if ($active_only) { $where[] = 'is_active = 1'; } $where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; $sql = "SELECT * FROM {$this->table_name} {$where_clause} ORDER BY sort_order ASC, model_name ASC"; if (!empty($values)) { $sql = $wpdb->prepare($sql, $values); } return $wpdb->get_results($sql); } public function get_chat_models($provider) { return $this->get_models($provider, self::TYPE_CHAT); } public function get_embedding_models($provider = null) { return $this->get_models($provider, self::TYPE_EMBEDDING); } public function get_models_for_dropdown($provider, $type = self::TYPE_CHAT) { $models = $this->get_models($provider, $type); $dropdown = array(); foreach ($models as $model) { $label = $model->model_name; if ($model->is_recommended) { $label .= ' ⭐'; } if (!empty($model->description)) { $label .= ' - ' . self::describe($model); } $dropdown[$model->model_id] = $label; } return $dropdown; } public function is_valid_model($model_id, $provider = null) { global $wpdb; $sql = "SELECT COUNT(*) FROM {$this->table_name} WHERE model_id = %s AND is_active = 1"; $values = array($model_id); if ($provider !== null) { $sql .= " AND provider = %s"; $values[] = $provider; } return $wpdb->get_var($wpdb->prepare($sql, $values)) > 0; } public function refresh_all_models() { $results = array( 'anthropic' => $this->refresh_anthropic_models(), 'voyage' => $this->refresh_voyage_models(), ); update_option('aieo_models_last_refresh', current_time('timestamp', true)); return $results; } public function refresh_anthropic_models() { $rooster_token = (string) get_option('aieo_rooster_api_key', ''); if (empty($rooster_token)) { return array('success' => false, 'message' => __('Rooster Content Connector token not configured — paste it in the Rooster One Pro.', 'ai-eshop-optimizer'), 'count' => 0); } $rooster_base = (string) get_option('aieo_rooster_hub_url', 'https://rooster.systems'); if ('' === $rooster_base) $rooster_base = 'https://rooster.systems'; $models_url = rtrim($rooster_base, '/') . '/wp-json/om-portal/v1/anthropic/models'; $response = wp_remote_get($models_url, array( 'timeout' => 30, 'headers' => array( 'X-OM-API-Key' => $rooster_token, 'Accept' => 'application/json', ), )); if (is_wp_error($response)) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Model Registry: rooster.systems unreachable - ' . $response->get_error_message()); } return array('success' => false, 'message' => $response->get_error_message(), 'count' => 0); } $code = wp_remote_retrieve_response_code($response); if ($code !== 200) { $body = wp_remote_retrieve_body($response); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Model Registry: rooster.systems returned ' . $code . ' - ' . $body); } $parsed = json_decode($body, true); $msg = (is_array($parsed) && !empty($parsed['message'])) ? (string) $parsed['message'] : sprintf( __( 'Proxy returned code %d', 'ai-eshop-optimizer' ), $code ); return array('success' => false, 'message' => $msg, 'count' => 0); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (empty($data['data'])) { return array('success' => false, 'message' => __('No models returned', 'ai-eshop-optimizer'), 'count' => 0); } $count = 0; $sort = 10; $listed = array(); foreach ($data['data'] as $model) { $id = $model['id'] ?? ''; if (strpos($id, 'claude') !== 0) { continue; } $name = $this->format_model_name($id, 'anthropic'); $max_tokens = $this->estimate_max_tokens($id); $this->insert_model( self::PROVIDER_ANTHROPIC, $id, $name, self::TYPE_CHAT, $model['description'] ?? '', $max_tokens, null, $sort === 10, $sort ); $listed[] = $id; $sort += 10; $count++; } if ($count > 0) { global $wpdb; $placeholders = implode(',', array_fill(0, count($listed), '%s')); $wpdb->query($wpdb->prepare("UPDATE {$this->table_name} SET is_active = 0 WHERE provider = %s AND model_type = %s AND model_id NOT IN ($placeholders)", array_merge(array(self::PROVIDER_ANTHROPIC, self::TYPE_CHAT), $listed))); } return array( 'success' => true, 'message' => sprintf(__('Fetched %1$d models; rooster.systems checked Anthropic %2$s', 'ai-eshop-optimizer'), $count, $this->portal_checked_phrase($data)), 'count' => $count ); } private function portal_checked_phrase($data) { $ts = (is_array($data) && !empty($data['checked_at'])) ? (int) $data['checked_at'] : 0; if ($ts <= 0) { return __('at an unknown time', 'ai-eshop-optimizer'); } $age = time() - $ts; if ($age < MINUTE_IN_SECONDS) { return __('just now', 'ai-eshop-optimizer'); } return sprintf(__('%s ago', 'ai-eshop-optimizer'), human_time_diff($ts)); } public function refresh_openai_models() { $api_key = get_option('aieo_chat_openai_api_key', ''); if (empty($api_key)) { return array('success' => false, 'message' => __('No API key configured', 'ai-eshop-optimizer'), 'count' => 0); } $response = wp_remote_get(self::OPENAI_MODELS_URL, array( 'timeout' => 30, 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json' ) )); if (is_wp_error($response)) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log('AIEO Model Registry: OpenAI API error - ' . $response->get_error_message()); } return array('success' => false, 'message' => $response->get_error_message(), 'count' => 0); } $code = wp_remote_retrieve_response_code($response); if ($code !== 200) { return array('success' => false, 'message' => sprintf( __( 'API returned code %d', 'ai-eshop-optimizer' ), $code ), 'count' => 0); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (empty($data['data'])) { return array('success' => false, 'message' => __('No models returned', 'ai-eshop-optimizer'), 'count' => 0); } $chat_count = 0; $embed_count = 0; $chat_sort = 10; $embed_sort = 10; $chat_prefixes = array('gpt-4', 'gpt-3.5', 'o1', 'o3', 'chatgpt'); $embed_prefixes = array('text-embedding'); $excluded = array('whisper', 'tts', 'dall-e', 'babbage', 'davinci', 'curie', 'instruct', 'search', 'similarity', 'code', 'edit', 'insert'); foreach ($data['data'] as $model) { $id = $model['id'] ?? ''; $skip = false; foreach ($excluded as $ex) { if (stripos($id, $ex) !== false) { $skip = true; break; } } if ($skip) continue; $is_chat = false; foreach ($chat_prefixes as $prefix) { if (strpos($id, $prefix) === 0) { $is_chat = true; break; } } $is_embed = false; foreach ($embed_prefixes as $prefix) { if (strpos($id, $prefix) === 0) { $is_embed = true; break; } } if ($is_chat) { $name = $this->format_model_name($id, 'openai'); $this->insert_model( self::PROVIDER_OPENAI, $id, $name, self::TYPE_CHAT, '', $this->estimate_max_tokens($id), null, $chat_sort === 10, $chat_sort ); $chat_sort += 10; $chat_count++; } elseif ($is_embed) { $name = $this->format_model_name($id, 'openai'); $dimensions = $this->estimate_dimensions($id); $this->insert_model( self::PROVIDER_OPENAI, $id, $name, self::TYPE_EMBEDDING, '', null, $dimensions, $embed_sort === 10, $embed_sort ); $embed_sort += 10; $embed_count++; } } $total = $chat_count + $embed_count; return array( 'success' => true, 'message' => sprintf(__('Fetched %1$d chat and %2$d embedding models', 'ai-eshop-optimizer'), $chat_count, $embed_count), 'count' => $total ); } public function refresh_voyage_models() { global $wpdb; $catalogue = array(); foreach ( self::voyage_catalogue() as $row ) { $catalogue[ $row[0] ] = $row; } $portal = $this->fetch_portal_voyage_models(); $live = is_array( $portal ); $known = array(); $count = 0; $sort = 10; $recommended = ''; foreach ( $catalogue as $id => $row ) { list( , $name, $description, $dimensions, $is_recommended, $status ) = $row; $known[] = $id; if ( 'retired' === $status ) { $this->set_model_active( self::PROVIDER_VOYAGE, $id, false ); continue; } $meta = ( $live && isset( $portal[ $id ] ) ) ? $portal[ $id ] : array(); if ( isset( $meta['served'] ) && ! $meta['served'] ) { $this->set_model_active( self::PROVIDER_VOYAGE, $id, false ); continue; } if ( isset( $meta['recommended'] ) ) { $is_recommended = (bool) $meta['recommended']; } if ( $is_recommended && '' === $recommended ) { $recommended = $id; } $this->insert_model( self::PROVIDER_VOYAGE, $id, ( ! empty( $meta['display_name'] ) && $meta['display_name'] !== $id ) ? $meta['display_name'] : $name, self::TYPE_EMBEDDING, ! empty( $meta['description'] ) ? $meta['description'] : $description, null, ! empty( $meta['dimensions'] ) ? (int) $meta['dimensions'] : $dimensions, $is_recommended, $sort ); $sort += 10; $count++; } if ( $live ) { foreach ( $portal as $id => $meta ) { if ( isset( $catalogue[ $id ] ) || in_array( $id, self::VOYAGE_PHANTOMS, true ) ) { continue; } $known[] = $id; $this->insert_model( self::PROVIDER_VOYAGE, $id, ( ! empty( $meta['display_name'] ) && $meta['display_name'] !== $id ) ? $meta['display_name'] : $this->format_model_name( $id, 'voyage' ), self::TYPE_EMBEDDING, (string) $meta['description'], null, ! empty( $meta['dimensions'] ) ? (int) $meta['dimensions'] : 1024, ! empty( $meta['recommended'] ), $sort ); $sort += 10; $count++; } } $placeholders = implode( ',', array_fill( 0, count( $known ), '%s' ) ); $wpdb->query( $wpdb->prepare( "UPDATE {$this->table_name} SET is_active = 0 WHERE provider = %s AND model_id NOT IN ($placeholders)", array_merge( array( self::PROVIDER_VOYAGE ), $known ) ) ); foreach ( self::VOYAGE_PHANTOMS as $ghost ) { $wpdb->delete( $this->table_name, array( 'provider' => self::PROVIDER_VOYAGE, 'model_id' => $ghost ), array( '%s', '%s' ) ); if ( '' !== $recommended && $ghost === (string) get_option( 'aieo_chat_embedding_model', '' ) ) { update_option( 'aieo_chat_embedding_model', $recommended ); } } return array( 'success' => true, 'message' => $live ? sprintf( __( 'Loaded %1$d Voyage models; rooster.systems verified them with Voyage %2$s', 'ai-eshop-optimizer' ), $count, $this->voyage_checked_phrase ) : sprintf( __( 'Loaded %1$d Voyage models from the bundled list (%2$s)', 'ai-eshop-optimizer' ), $count, $portal ), 'count' => $count, ); } private function fetch_portal_voyage_models() { $token = (string) get_option( 'aieo_rooster_api_key', '' ); if ( '' === $token ) { return __( 'no Content Connector token', 'ai-eshop-optimizer' ); } $base = (string) get_option( 'aieo_rooster_hub_url', 'https://rooster.systems' ); if ( '' === $base ) { $base = 'https://rooster.systems'; } $response = wp_remote_get( rtrim( $base, '/' ) . '/wp-json/om-portal/v1/' . self::VOYAGE_MODELS_PATH, array( 'timeout' => 20, 'headers' => array( 'X-OM-API-Key' => $token, 'Accept' => 'application/json', ), ) ); if ( is_wp_error( $response ) ) { return $response->get_error_message(); } $code = (int) wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { return sprintf( __( 'rooster.systems answered %d', 'ai-eshop-optimizer' ), $code ); } $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $data['data'] ) || ! is_array( $data['data'] ) ) { return __( 'rooster.systems returned no models', 'ai-eshop-optimizer' ); } $this->voyage_checked_phrase = $this->portal_checked_phrase( $data ); $out = array(); foreach ( $data['data'] as $row ) { $id = isset( $row['id'] ) ? sanitize_text_field( (string) $row['id'] ) : ''; if ( '' === $id || 0 !== strpos( $id, 'voyage-' ) ) { continue; } $out[ $id ] = array( 'display_name' => isset( $row['display_name'] ) ? sanitize_text_field( (string) $row['display_name'] ) : '', 'description' => isset( $row['description'] ) ? sanitize_text_field( (string) $row['description'] ) : '', 'dimensions' => isset( $row['dimensions'] ) ? (int) $row['dimensions'] : 0, ); if ( array_key_exists( 'recommended', $row ) ) { $out[ $id ]['recommended'] = (bool) $row['recommended']; } if ( array_key_exists( 'served', $row ) ) { $out[ $id ]['served'] = (bool) $row['served']; } } return $out; } private function format_model_name($id, $provider) { switch ($provider) { case 'anthropic': $name = str_replace('claude-', 'Claude ', $id); $name = preg_replace('/-\d{8}$/', '', $name); $name = str_replace('-', ' ', $name); $name = preg_replace('/(\d)\s+(\d)/', '$1.$2', $name); return ucwords($name); case 'openai': $name = strtoupper(substr($id, 0, 3)) . substr($id, 3); $name = str_replace('-', ' ', $name); $name = str_replace('text embedding', 'Text Embedding', $name); return ucwords($name); case 'voyage': $name = str_replace('-', ' ', $id); return ucwords($name); default: return $id; } } private function estimate_max_tokens($id) { if (strpos($id, 'opus-4-5') !== false || strpos($id, 'sonnet-4-5') !== false) { return 16384; } if (strpos($id, 'opus-4') !== false || strpos($id, 'sonnet-4') !== false) { return 8192; } if (strpos($id, 'o1') !== false || strpos($id, 'o3') !== false) { return 32768; } if (strpos($id, 'gpt-4o') !== false) { return 16384; } if (strpos($id, 'gpt-4') !== false) { return 8192; } return 4096; } private function estimate_dimensions($id) { if (strpos($id, '3-large') !== false) { return 3072; } if (strpos($id, '3-small') !== false || strpos($id, 'ada-002') !== false) { return 1536; } if (strpos($id, 'large') !== false) { return 1536; } if (strpos($id, 'lite') !== false) { return 512; } if (strpos($id, 'voyage-3.5') !== false || strpos($id, 'voyage-3') !== false) { return 1024; } return 1024; } public function get_last_refresh_info() { $timestamp = get_option('aieo_models_last_refresh', 0); return array( 'timestamp' => $timestamp, 'human' => $timestamp > 0 ? human_time_diff($timestamp, current_time('timestamp', true)) . ' ' . __('ago', 'ai-eshop-optimizer') : __('Never', 'ai-eshop-optimizer'), 'date' => $timestamp > 0 ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), $timestamp) : '' ); } public function ajax_refresh_models() { check_ajax_referer('aieo_refresh_models', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer')), 403); } $results = $this->refresh_all_models(); $total_success = 0; $total_count = 0; $messages = array(); foreach ($results as $provider => $result) { if ($result['success']) { $total_success++; $total_count += $result['count']; } $messages[] = ucfirst($provider) . ': ' . $result['message']; } if ($total_success > 0) { wp_send_json_success(array( 'message' => sprintf(__('Refreshed %1$d models from %2$d providers', 'ai-eshop-optimizer'), $total_count, $total_success), 'details' => $messages, 'results' => $results, 'refresh_info' => $this->get_last_refresh_info() )); } else { wp_send_json_error(array( 'message' => __('Failed to refresh models from any provider', 'ai-eshop-optimizer'), 'details' => $messages, 'results' => $results )); } } public function get_anthropic_models() { $models = $this->get_chat_models(self::PROVIDER_ANTHROPIC); $result = array(); foreach ($models as $model) { $result[$model->model_id] = array( 'name' => $model->model_name, 'description' => $model->description, 'max_tokens' => $model->max_tokens, 'recommended' => (bool) $model->is_recommended ); } return $result; } public function get_openai_models() { $models = $this->get_chat_models(self::PROVIDER_OPENAI); $result = array(); foreach ($models as $model) { $result[$model->model_id] = array( 'name' => $model->model_name, 'description' => $model->description, 'max_tokens' => $model->max_tokens, 'recommended' => (bool) $model->is_recommended ); } return $result; } public function ajax_validate_api_keys() { check_ajax_referer('aieo_validate_api_keys', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer')), 403); } $results = array( 'anthropic' => $this->validate_anthropic_key(), 'openai' => $this->validate_openai_key(), 'voyage' => $this->validate_voyage_key() ); wp_send_json_success($results); } private function validate_anthropic_key() { $api_key = get_option('aieo_chat_anthropic_api_key', ''); if (empty($api_key)) { return array('not_configured' => true); } $response = wp_remote_post('https://api.anthropic.com/v1/messages', array( 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json', 'x-api-key' => $api_key, 'anthropic-version' => '2023-06-01' ), 'body' => wp_json_encode(array( 'model' => 'claude-3-haiku-20240307', 'max_tokens' => 1, 'messages' => array( array('role' => 'user', 'content' => 'Hi') ) )) )); if (is_wp_error($response)) { return array('valid' => false, 'error' => $response->get_error_message()); } $code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($code === 400 || $code === 403 || $code === 429) { $error_type = isset($body['error']['type']) ? $body['error']['type'] : ''; $error_message = isset($body['error']['message']) ? $body['error']['message'] : ''; if (strpos($error_type, 'credit') !== false || strpos($error_message, 'credit') !== false || strpos($error_message, 'balance') !== false || strpos($error_type, 'billing') !== false) { return array('valid' => true, 'credits_exhausted' => true); } if ($code === 429) { return array('valid' => true, 'credits_exhausted' => false); } } if ($code === 401) { return array('valid' => false, 'error' => __('Invalid API key', 'ai-eshop-optimizer')); } if ($code === 200) { return array('valid' => true, 'credits_exhausted' => false); } $error_msg = isset($body['error']['message']) ? $body['error']['message'] : __('Unknown error', 'ai-eshop-optimizer'); return array('valid' => false, 'error' => $error_msg); } private function validate_openai_key() { $api_key = get_option('aieo_chat_openai_api_key', ''); if (empty($api_key)) { return array('not_configured' => true); } $response = wp_remote_get('https://api.openai.com/v1/models', array( 'timeout' => 15, 'headers' => array( 'Authorization' => 'Bearer ' . $api_key ) )); if (is_wp_error($response)) { return array('valid' => false, 'error' => $response->get_error_message()); } $code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($code === 429 || $code === 402) { $error_code = isset($body['error']['code']) ? $body['error']['code'] : ''; $error_message = isset($body['error']['message']) ? $body['error']['message'] : ''; if ($error_code === 'insufficient_quota' || strpos($error_message, 'quota') !== false || strpos($error_message, 'exceeded') !== false || strpos($error_message, 'billing') !== false) { return array('valid' => true, 'credits_exhausted' => true); } if ($code === 429 && $error_code === 'rate_limit_exceeded') { return array('valid' => true, 'credits_exhausted' => false); } } if ($code === 401) { return array('valid' => false, 'error' => __('Invalid API key', 'ai-eshop-optimizer')); } if ($code === 200) { return array('valid' => true, 'credits_exhausted' => false); } $error_msg = isset($body['error']['message']) ? $body['error']['message'] : __('Unknown error', 'ai-eshop-optimizer'); return array('valid' => false, 'error' => $error_msg); } private function validate_voyage_key() { $api_key = get_option('aieo_chat_voyage_api_key', ''); if (empty($api_key)) { return array('not_configured' => true); } $response = wp_remote_post('https://api.voyageai.com/v1/embeddings', array( 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_key ), 'body' => wp_json_encode(array( 'model' => 'voyage-4-lite', 'input' => 'test' )) )); if (is_wp_error($response)) { return array('valid' => false, 'error' => $response->get_error_message()); } $code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($code === 402 || $code === 429) { $error_message = isset($body['detail']) ? $body['detail'] : (isset($body['error']['message']) ? $body['error']['message'] : ''); if (strpos(strtolower($error_message), 'credit') !== false || strpos(strtolower($error_message), 'quota') !== false || strpos(strtolower($error_message), 'billing') !== false || strpos(strtolower($error_message), 'payment') !== false) { return array('valid' => true, 'credits_exhausted' => true); } if ($code === 429) { return array('valid' => true, 'credits_exhausted' => false); } } if ($code === 401) { return array('valid' => false, 'error' => __('Invalid API key', 'ai-eshop-optimizer')); } if ($code === 200) { return array('valid' => true, 'credits_exhausted' => false); } $error_msg = isset($body['detail']) ? $body['detail'] : (isset($body['error']['message']) ? $body['error']['message'] : __('Unknown error', 'ai-eshop-optimizer')); return array('valid' => false, 'error' => $error_msg); } public function sampling_params() { return array('temperature', 'top_p', 'top_k'); } public function get_unsupported_params($model_id) { global $wpdb; $meta = $wpdb->get_var($wpdb->prepare("SELECT metadata FROM {$this->table_name} WHERE model_id = %s LIMIT 1", $model_id)); if (empty($meta)) { return array(); } $decoded = json_decode($meta, true); if (!is_array($decoded) || empty($decoded['unsupported_params']) || !is_array($decoded['unsupported_params'])) { return array(); } return array_values(array_unique(array_map('strval', $decoded['unsupported_params']))); } public function set_unsupported_params($model_id, array $params) { global $wpdb; $row = $wpdb->get_row($wpdb->prepare("SELECT id, metadata FROM {$this->table_name} WHERE model_id = %s LIMIT 1", $model_id)); if (!$row) { return false; } $meta = array(); if (!empty($row->metadata)) { $decoded = json_decode($row->metadata, true); if (is_array($decoded)) { $meta = $decoded; } } $meta['unsupported_params'] = array_values(array_unique(array_map('strval', $params))); return false !== $wpdb->update( $this->table_name, array('metadata' => wp_json_encode($meta)), array('id' => (int) $row->id), array('%s'), array('%d') ); } public function mark_param_unsupported($model_id, $param) { $current = $this->get_unsupported_params($model_id); if (!in_array($param, $current, true)) { $current[] = $param; $this->set_unsupported_params($model_id, $current); } } public function detect_params($model_id) { if (!class_exists('AI_eShop_Chat_Anthropic_API')) { return array('error' => __('Anthropic API class not loaded', 'ai-eshop-optimizer')); } $api = AI_eShop_Chat_Anthropic_API::get_instance(); $msgs = array(array('role' => 'user', 'content' => 'Reply with exactly one word: hello')); $base = array('model' => $model_id, 'max_tokens' => 16, 'messages' => $msgs); $b = $api->raw_messages_probe($base); if (intval($b['code']) !== 200) { return array('error' => sprintf(__('Baseline call failed (HTTP %1$s): %2$s', 'ai-eshop-optimizer'), $b['code'], $b['error'])); } $supported = array(); $unsupported = array(); $probes = array('temperature' => 0.5, 'top_p' => 0.9, 'top_k' => 40); foreach ($probes as $param => $val) { $body = $base; $body[$param] = $val; $r = $api->raw_messages_probe($body); if (intval($r['code']) === 200) { $supported[] = $param; } elseif (intval($r['code']) === 400 && stripos((string) $r['error'], 'deprecated') !== false) { $unsupported[] = $param; } else { $supported[] = $param; } } $this->set_unsupported_params($model_id, $unsupported); return array('supported' => $supported, 'unsupported' => $unsupported, 'base_ok' => true, 'error' => ''); } public function ajax_model_test() { check_ajax_referer('aieo_chat_model_tester', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer')), 403); } $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : ''; $message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : ''; if ('' === $model) { wp_send_json_error(array('message' => __('No model selected', 'ai-eshop-optimizer'))); } if ('' === $message) { $message = 'Reply with exactly one word: hello'; } $max_tokens = isset($_POST['max_tokens']) ? max(1, min(2000, (int) $_POST['max_tokens'])) : 100; $body = array( 'model' => $model, 'max_tokens' => $max_tokens, 'messages' => array(array('role' => 'user', 'content' => $message)), ); if (isset($_POST['use_temperature']) && '1' === $_POST['use_temperature']) { $body['temperature'] = (float) wp_unslash($_POST['temperature']); } if (isset($_POST['use_top_p']) && '1' === $_POST['use_top_p']) { $body['top_p'] = (float) wp_unslash($_POST['top_p']); } if (isset($_POST['use_top_k']) && '1' === $_POST['use_top_k']) { $body['top_k'] = (int) wp_unslash($_POST['top_k']); } if (!class_exists('AI_eShop_Chat_Anthropic_API')) { wp_send_json_error(array('message' => __('Anthropic API class not loaded', 'ai-eshop-optimizer'))); } $probe = AI_eShop_Chat_Anthropic_API::get_instance()->raw_messages_probe($body); $text = isset($probe['json']['content'][0]['text']) ? $probe['json']['content'][0]['text'] : ''; wp_send_json_success(array( 'request' => $body, 'http_code' => (int) $probe['code'], 'latency_ms' => (int) $probe['latency_ms'], 'error' => (string) $probe['error'], 'text' => $text, 'raw' => (string) $probe['body'], )); } public function ajax_model_detect() { check_ajax_referer('aieo_chat_model_tester', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'ai-eshop-optimizer')), 403); } $model = isset($_POST['model']) ? sanitize_text_field(wp_unslash($_POST['model'])) : ''; if ('' === $model) { wp_send_json_error(array('message' => __('No model selected', 'ai-eshop-optimizer'))); } $result = $this->detect_params($model); if (!empty($result['error'])) { wp_send_json_error(array('message' => $result['error'])); } wp_send_json_success($result); } } AI_eShop_Chat_Model_Registry::get_instance();