api_key = MetaForge_AI_API_KEY; } else { $stored_key = $settings['api_key'] ?? ''; $this->api_key = self::decrypt_key( $stored_key ); } $this->model = $settings['model'] ?? 'gpt-4o'; } /** * Derive the local secret used to encrypt/decrypt stored API keys. * Prefers the site's AUTH_KEY (set in wp-config.php); falls back to * wp_salt(), which WordPress generates and persists uniquely per * install, so the fallback is never a predictable shared value. */ private static function get_encryption_secret() { if ( defined( 'AUTH_KEY' ) && AUTH_KEY !== '' ) { return AUTH_KEY; } return wp_salt( 'auth' ); } public static function encrypt_key( $key ) { if ( empty( $key ) ) { return ''; } if ( strpos( $key, 'enc:' ) === 0 ) { return $key; // Already encrypted } $secret = self::get_encryption_secret(); $iv = substr( hash( 'sha256', $secret ), 0, 16 ); $encrypted = openssl_encrypt( $key, 'AES-256-CBC', $secret, 0, $iv ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Binary AES-256-CBC ciphertext is base64-encoded so it can be safely stored as a text option value; this is encoding, not code obfuscation. return $encrypted !== false ? 'enc:' . base64_encode( $encrypted ) : $key; } public static function decrypt_key( $encrypted_key ) { if ( empty( $encrypted_key ) ) { return ''; } if ( strpos( $encrypted_key, 'enc:' ) !== 0 ) { return $encrypted_key; // Unencrypted legacy key } $secret = self::get_encryption_secret(); $iv = substr( hash( 'sha256', $secret ), 0, 16 ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Reversing the base64 encoding applied in encrypt_key() above to recover the stored AES-256-CBC ciphertext; this is decoding, not code obfuscation. $raw = base64_decode( substr( $encrypted_key, 4 ), true ); if ( $raw === false ) { return $encrypted_key; } $decrypted = openssl_decrypt( $raw, 'AES-256-CBC', $secret, 0, $iv ); return $decrypted !== false ? $decrypted : $encrypted_key; } public function is_ready() { $decrypted_key = trim( self::decrypt_key( $this->api_key ) ); if ( ! empty( $decrypted_key ) && $decrypted_key !== 'wp_ai_core' ) { return true; } if ( function_exists( 'wp_ai_client_prompt' ) ) { return true; } return false; } public function get_model() { return $this->model; } // Models that use the new OpenAI Responses API (/v1/responses) private $responses_api_models = array( 'gpt-4o-responses', ); public function generate( $system_prompt, $user_prompt, $json_mode = false ) { $decrypted_key = trim( self::decrypt_key( $this->api_key ) ); $has_direct_key = ! empty( $decrypted_key ) && $decrypted_key !== 'wp_ai_core'; $is_core_ai_available = function_exists( 'wp_ai_client_prompt' ); if ( ! $has_direct_key && ! $is_core_ai_available ) { return new WP_Error( 'metaforge_ai_error', 'AI is not configured. Please enter an API key under MetaForge -> AI Settings or connect an AI provider under Settings -> Connectors.' ); } // Determine priority: if model is 'auto_wp_ai' or no direct key, try Core AI first. // Otherwise, try the direct provider first. $prefer_core_ai = $is_core_ai_available && ( $this->model === 'auto_wp_ai' || ! $has_direct_key ); if ( $prefer_core_ai ) { $core_result = $this->call_core_ai( $system_prompt, $user_prompt, $json_mode ); if ( ! is_wp_error( $core_result ) && ! empty( $core_result ) ) { return $core_result; } // Fallback: If WordPress Connector returned an error and user entered an API key in AI Settings, use it! if ( $has_direct_key ) { $direct_result = $this->call_direct_provider( $system_prompt, $user_prompt, $json_mode, $decrypted_key ); if ( ! is_wp_error( $direct_result ) && ! empty( $direct_result ) ) { return $direct_result; } } return $core_result; } else { // Direct provider is preferred $direct_result = null; if ( $has_direct_key ) { $direct_result = $this->call_direct_provider( $system_prompt, $user_prompt, $json_mode, $decrypted_key ); if ( ! is_wp_error( $direct_result ) && ! empty( $direct_result ) ) { return $direct_result; } } // Fallback: If direct provider failed and WordPress AI Connector is available, try it! if ( $is_core_ai_available ) { $core_result = $this->call_core_ai( $system_prompt, $user_prompt, $json_mode ); if ( ! is_wp_error( $core_result ) && ! empty( $core_result ) ) { return $core_result; } } return is_wp_error( $direct_result ) ? $direct_result : new WP_Error( 'metaforge_ai_error', 'Unable to complete AI generation. Please check your API settings or WordPress Connectors.' ); } } /** * Call WordPress Core AI Client (WordPress 7.0+ Connectors) */ private function call_core_ai( $system_prompt, $user_prompt, $json_mode ) { if ( ! function_exists( 'wp_ai_client_prompt' ) ) { return new WP_Error( 'metaforge_core_ai_unavailable', 'WordPress Core AI Client is not available on this installation.' ); } try { $builder = wp_ai_client_prompt( $user_prompt ) ->using_system_instruction( $system_prompt ) ->using_temperature( 0.7 ); if ( $json_mode ) { $builder->as_json_response(); } $pref_model = ( ! empty( $this->model ) && $this->model !== 'auto_wp_ai' ) ? $this->model : ''; if ( $pref_model === 'gemini-3.5-flash-lite' || $pref_model === 'gemini-3.6-flash' ) { $pref_model = 'gemini-2.0-flash'; } $target_builder = $builder; if ( ! empty( $pref_model ) ) { try { $builder_with_pref = clone $builder; $builder_with_pref->using_model_preference( $pref_model ); $target_builder = $builder_with_pref; } catch ( \Throwable $t ) { $target_builder = $builder; } } $result = $target_builder->generate_text(); // Fallback to base builder without model preference if preferred model errors if ( is_wp_error( $result ) && $target_builder !== $builder ) { try { $fallback_result = $builder->generate_text(); if ( ! is_wp_error( $fallback_result ) && ! empty( $fallback_result ) ) { $result = $fallback_result; } } catch ( \Throwable $t ) { // Keep original error } } if ( ! is_wp_error( $result ) && ! empty( $result ) ) { return is_string( $result ) ? $result : wp_json_encode( $result ); } if ( is_wp_error( $result ) ) { $err_msg = $result->get_error_message(); if ( stripos( $err_msg, 'approved' ) !== false || stripos( $err_msg, 'approval' ) !== false ) { $connector_url = admin_url( 'options-connectors.php' ); $approval_url = admin_url( 'tools.php?page=ai-connector-approval' ); $hint = 'Connector Approval Required: ' . esc_html( $err_msg ) . '

' . '👉 To fix: Go to Tools > AI Connector Approval (or Settings > Connectors), approve MetaForge Engine for your connected provider, and try again.'; return new WP_Error( 'metaforge_connector_approval', $hint ); } return $result; } return new WP_Error( 'metaforge_core_ai_empty', 'Empty response from WordPress AI Connector.' ); } catch ( \Throwable $e ) { return new WP_Error( 'metaforge_wp_ai_error', $e->getMessage() ); } } /** * Call direct AI provider with user-supplied API key */ private function call_direct_provider( $system_prompt, $user_prompt, $json_mode, $decrypted_key ) { $model = strtolower( $this->model ); // Auto-detect provider if model was set to auto_wp_ai if ( $model === 'auto_wp_ai' || empty( $model ) ) { if ( strpos( $decrypted_key, 'AIzaSy' ) === 0 || strpos( $decrypted_key, 'AQ.' ) === 0 ) { $model = 'gemini-2.0-flash'; } elseif ( strpos( $decrypted_key, 'gsk_' ) === 0 ) { $model = 'llama-3.3-70b-versatile'; } elseif ( strpos( $decrypted_key, 'sk-ant-' ) === 0 ) { $model = 'claude-3-5-haiku-latest'; } elseif ( strpos( $decrypted_key, 'sk-or-v1-' ) === 0 ) { $model = 'openrouter/auto'; } else { $model = 'gpt-4o-mini'; } $this->model = $model; } $is_groq_key = ( strpos( $decrypted_key, 'gsk_' ) === 0 ); $is_gemini_key = ( strpos( $decrypted_key, 'AIzaSy' ) === 0 || strpos( $decrypted_key, 'AQ.' ) === 0 ); $is_claude_key = ( strpos( $decrypted_key, 'sk-ant-' ) === 0 ); $is_or_key = ( strpos( $decrypted_key, 'sk-or-v1-' ) === 0 ); if ( strpos( $model, 'deepseek' ) !== false && strpos( $model, 'distill' ) === false ) { return $this->call_deepseek( $system_prompt, $user_prompt, $json_mode ); } elseif ( $is_or_key || strpos( $model, 'openrouter' ) !== false ) { return $this->call_openrouter( $system_prompt, $user_prompt, $json_mode ); } elseif ( $is_claude_key || strpos( $model, 'claude' ) !== false ) { return $this->call_anthropic( $system_prompt, $user_prompt, $json_mode ); } elseif ( $is_gemini_key || strpos( $model, 'gemini' ) !== false ) { return $this->call_gemini( $system_prompt, $user_prompt, $json_mode ); } elseif ( $is_groq_key || strpos( $model, 'llama' ) !== false || strpos( $model, 'distill' ) !== false || strpos( $model, 'qwen' ) !== false || strpos( $model, 'gpt-oss' ) !== false || strpos( $model, 'groq' ) !== false ) { return $this->call_groq( $system_prompt, $user_prompt, $json_mode ); } elseif ( in_array( $this->model, $this->responses_api_models, true ) ) { return $this->call_openai_responses( $system_prompt, $user_prompt, $json_mode ); } else { return $this->call_openai( $system_prompt, $user_prompt, $json_mode ); } } // ── OpenAI Chat Completions API (gpt-4o, gpt-4o-mini, gpt-4.5, o3-mini, o1, etc.) ── private function call_openai( $system_prompt, $user_prompt, $json_mode ) { // phpcs:ignore PluginCheck.CodeAnalysis.AIProvider.DirectIntegration -- Direct integration required for user-supplied OpenAI keys and WP < 7.0 compatibility. $url = 'https://api.openai.com/v1/chat/completions'; $decrypted_key = self::decrypt_key( $this->api_key ); $body = array( 'model' => $this->model, 'messages' => array( array( 'role' => 'system', 'content' => $system_prompt, ), array( 'role' => 'user', 'content' => $user_prompt, ), ), 'temperature' => ( strpos( $this->model, 'o1' ) !== false || strpos( $this->model, 'o3' ) !== false ) ? 1 : 0.7, ); if ( $json_mode ) { $body['response_format'] = array( 'type' => 'json_object' ); } $args = array( 'body' => wp_json_encode( $body ), 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $decrypted_key, ), 'timeout' => 60, 'data_format' => 'body', ); $response = wp_remote_post( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( isset( $data['error'] ) ) { $err = $data['error']; $message = $err['message'] ?? 'Unknown OpenAI error.'; $type = $err['type'] ?? ''; $code = $err['code'] ?? ''; $is_quota = ( $code === 'insufficient_quota' || $type === 'insufficient_quota' || strpos( $message, 'exceeded your current quota' ) !== false || strpos( $message, 'billing' ) !== false ); if ( $is_quota ) { $hint = 'Your OpenAI account has no remaining credits. '; $hint .= 'Add a payment method or purchase credits at '; $hint .= 'platform.openai.com/billing.'; return new WP_Error( 'metaforge_openai_quota', $hint, array( 'retry_after' => 0 ) ); } return new WP_Error( 'metaforge_openai_error', $message ); } return $data['choices'][0]['message']['content'] ?? ''; } // ── OpenAI Responses API (gpt-5-nano and newer models) ─────────────────── private function call_openai_responses( $system_prompt, $user_prompt, $json_mode ) { // phpcs:ignore PluginCheck.CodeAnalysis.AIProvider.DirectIntegration -- Direct integration required for user-supplied OpenAI keys and WP < 7.0 compatibility. $url = 'https://api.openai.com/v1/responses'; $decrypted_key = self::decrypt_key( $this->api_key ); $input = $system_prompt . "\n\n" . $user_prompt; $body = array( 'model' => $this->model, 'input' => $input, 'store' => false, ); if ( $json_mode ) { $body['input'] = $input . "\n\nRespond ONLY with valid raw JSON."; } $args = array( 'body' => wp_json_encode( $body ), 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $decrypted_key, ), 'timeout' => 60, 'data_format' => 'body', ); $response = wp_remote_post( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( isset( $data['error'] ) ) { $err = $data['error']; $message = $err['message'] ?? 'Unknown OpenAI error.'; $type = $err['type'] ?? ''; $code = $err['code'] ?? ''; $is_quota = ( $code === 'insufficient_quota' || $type === 'insufficient_quota' || strpos( $message, 'exceeded your current quota' ) !== false || strpos( $message, 'billing' ) !== false ); if ( $is_quota ) { $hint = 'Your OpenAI account has no remaining credits. '; $hint .= 'Add a payment method or purchase credits at '; $hint .= 'platform.openai.com/billing.'; return new WP_Error( 'metaforge_openai_quota', $hint, array( 'retry_after' => 0 ) ); } return new WP_Error( 'metaforge_openai_error', $message ); } return $data['output_text'] ?? ( $data['output'][0]['content'][0]['text'] ?? '' ); } // ── DeepSeek API (deepseek-chat V3 / deepseek-reasoner R1) ──────────────── private function call_deepseek( $system_prompt, $user_prompt, $json_mode ) { // phpcs:ignore PluginCheck.CodeAnalysis.AIProvider.DirectIntegration -- Direct integration required for user-supplied DeepSeek keys and WP < 7.0 compatibility. $url = 'https://api.deepseek.com/v1/chat/completions'; $decrypted_key = self::decrypt_key( $this->api_key ); $body = array( 'model' => $this->model, 'messages' => array( array( 'role' => 'system', 'content' => $system_prompt, ), array( 'role' => 'user', 'content' => $user_prompt, ), ), 'temperature' => 0.7, ); if ( $json_mode ) { $body['response_format'] = array( 'type' => 'json_object' ); } $args = array( 'body' => wp_json_encode( $body ), 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $decrypted_key, ), 'timeout' => 60, 'data_format' => 'body', ); $response = wp_remote_post( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( isset( $data['error'] ) ) { $msg = $data['error']['message'] ?? 'DeepSeek API Error'; if ( strpos( strtolower( $msg ), 'insufficient balance' ) !== false || strpos( strtolower( $msg ), 'balance' ) !== false ) { $msg = 'Your DeepSeek account has insufficient balance. Please top up credits at platform.deepseek.com/top_up.'; } return new WP_Error( 'metaforge_deepseek_error', $msg ); } return $data['choices'][0]['message']['content'] ?? ''; } // ── OpenRouter API ──────────────────────────────────────────────────────── private function call_openrouter( $system_prompt, $user_prompt, $json_mode ) { // phpcs:ignore PluginCheck.CodeAnalysis.AIProvider.DirectIntegration -- Direct integration required for user-supplied OpenRouter keys and WP < 7.0 compatibility. $url = 'https://openrouter.ai/api/v1/chat/completions'; $decrypted_key = self::decrypt_key( $this->api_key ); $real_model = str_replace( 'openrouter/', '', $this->model ); $body = array( 'model' => $real_model === 'auto' ? 'openai/gpt-4o-mini' : $real_model, 'messages' => array( array( 'role' => 'system', 'content' => $system_prompt, ), array( 'role' => 'user', 'content' => $user_prompt, ), ), 'temperature' => 0.7, ); if ( $json_mode ) { $body['response_format'] = array( 'type' => 'json_object' ); } $args = array( 'body' => wp_json_encode( $body ), 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $decrypted_key, 'HTTP-Referer' => home_url(), 'X-Title' => 'MetaForge Engine', ), 'timeout' => 60, 'data_format' => 'body', ); $response = wp_remote_post( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( isset( $data['error'] ) ) { $msg = $data['error']['message'] ?? 'OpenRouter API Error'; return new WP_Error( 'metaforge_openrouter_error', $msg ); } return $data['choices'][0]['message']['content'] ?? ''; } // ── Groq API ──────────────────────────────────────────────────────────── private function call_groq( $system_prompt, $user_prompt, $json_mode ) { // phpcs:ignore PluginCheck.CodeAnalysis.AIProvider.DirectIntegration -- Direct integration required for user-supplied Groq keys and WP < 7.0 compatibility. $url = 'https://api.groq.com/openai/v1/chat/completions'; $decrypted_key = self::decrypt_key( $this->api_key ); $model = $this->model; $groq_migration = array( 'llama-3.1-8b-instant' => 'openai/gpt-oss-120b', 'llama-3.3-70b-versatile' => 'openai/gpt-oss-120b', 'deepseek-r1-distill-qwen-32b' => 'openai/gpt-oss-120b', ); if ( isset( $groq_migration[ $model ] ) ) { $model = $groq_migration[ $model ]; } $body = array( 'model' => $model, 'messages' => array( array( 'role' => 'system', 'content' => $system_prompt, ), array( 'role' => 'user', 'content' => $user_prompt, ), ), 'temperature' => 0.7, ); if ( $json_mode ) { $body['response_format'] = array( 'type' => 'json_object' ); } $args = array( 'body' => wp_json_encode( $body ), 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $decrypted_key, ), 'timeout' => 60, 'data_format' => 'body', ); $response = wp_remote_post( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $response_body = wp_remote_retrieve_body( $response ); $data = json_decode( $response_body, true ); if ( isset( $data['error'] ) ) { $err = $data['error']; $msg = $err['message'] ?? 'Unknown Groq API error.'; // If the model does not exist or has been decommissioned, automatically query Groq's active models catalog if ( strpos( $msg, 'does not exist' ) !== false || strpos( $msg, 'decommissioned' ) !== false || strpos( $msg, 'do not have access' ) !== false ) { // phpcs:ignore PluginCheck.CodeAnalysis.AIProvider.DirectIntegration -- Direct integration required for user-supplied Groq keys and WP < 7.0 compatibility. $models_url = str_replace( '/chat/completions', '/models', $url ); $models_res = wp_remote_get( $models_url, array( 'headers' => array( 'Authorization' => 'Bearer ' . $decrypted_key ), 'timeout' => 15, ) ); if ( ! is_wp_error( $models_res ) ) { $models_data = json_decode( wp_remote_retrieve_body( $models_res ), true ); if ( ! empty( $models_data['data'] ) && is_array( $models_data['data'] ) ) { foreach ( $models_data['data'] as $m_item ) { $mid = $m_item['id'] ?? ''; // Pick first active text-generation model if ( $mid && strpos( $mid, 'whisper' ) === false && strpos( $mid, 'guard' ) === false && $mid !== $model ) { $body['model'] = $mid; $args['body'] = wp_json_encode( $body ); $retry_res = wp_remote_post( $url, $args ); if ( ! is_wp_error( $retry_res ) ) { $retry_data = json_decode( wp_remote_retrieve_body( $retry_res ), true ); if ( isset( $retry_data['choices'][0]['message']['content'] ) ) { return $retry_data['choices'][0]['message']['content']; } } break; } } } } } return new WP_Error( 'metaforge_groq_error', $msg ); } return $data['choices'][0]['message']['content'] ?? ''; } // ── Anthropic Claude API ──────────────────────────────────────────────── private function call_anthropic( $system_prompt, $user_prompt, $json_mode ) { $decrypted_key = self::decrypt_key( $this->api_key ); $model_map = array( 'claude-3-7-sonnet' => 'claude-3-7-sonnet-20250219', 'claude-3-7-sonnet-20250219' => 'claude-3-7-sonnet-20250219', 'claude-3-5-sonnet' => 'claude-3-5-sonnet-20241022', 'claude-3-5-sonnet-20241022' => 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku' => 'claude-3-5-haiku-20241022', 'claude-3-5-haiku-20241022' => 'claude-3-5-haiku-20241022', 'claude-3-opus' => 'claude-3-opus-20240229', 'claude-3-opus-20240229' => 'claude-3-opus-20240229', ); $model_string = $model_map[ $this->model ] ?? ( $this->model ?: 'claude-3-5-sonnet-20241022' ); // phpcs:ignore PluginCheck.CodeAnalysis.AIProvider.DirectIntegration -- Direct integration required for user-supplied Anthropic keys and WP < 7.0 compatibility. $url = 'https://api.anthropic.com/v1/messages'; $messages = array( array( 'role' => 'user', 'content' => $user_prompt, ), ); if ( $json_mode ) { $messages[] = array( 'role' => 'assistant', 'content' => '{', ); } $body = array( 'model' => $model_string, 'system' => $system_prompt, 'messages' => $messages, 'max_tokens' => 4096, 'temperature' => 0.7, ); $args = array( 'body' => wp_json_encode( $body ), 'headers' => array( 'Content-Type' => 'application/json', 'x-api-key' => $decrypted_key, 'anthropic-version' => '2023-06-01', ), 'timeout' => 60, ); $response = wp_remote_post( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $response_body = wp_remote_retrieve_body( $response ); $data = json_decode( $response_body, true ); if ( isset( $data['error'] ) ) { return new WP_Error( 'metaforge_anthropic_error', $data['error']['message'] ); } $content = $data['content'][0]['text'] ?? ''; if ( $json_mode ) { $content = '{' . $content; } return $content; } // ── Google Gemini API (SECURE HEADER AUTH) ────────────────────────────── private function call_gemini( $system_prompt, $user_prompt, $json_mode ) { $model_map = array( 'gemini-3.5-flash-lite' => 'gemini-2.0-flash', 'gemini-3.6-flash' => 'gemini-2.0-flash', 'gemini-2.5-flash' => 'gemini-2.5-flash', 'gemini-2.0-flash' => 'gemini-2.0-flash', 'gemini-2.0-flash-lite' => 'gemini-2.0-flash-lite', 'gemini-1.5-flash' => 'gemini-1.5-flash', 'gemini-1.5-pro' => 'gemini-1.5-pro', ); $model_name = $model_map[ $this->model ] ?? 'gemini-2.0-flash'; if ( empty( $model_name ) || strpos( $model_name, 'gemini' ) === false ) { $model_name = 'gemini-2.0-flash'; } $model_name = str_replace( 'models/', '', $model_name ); $decrypted_key = trim( self::decrypt_key( $this->api_key ) ); if ( empty( $decrypted_key ) ) { return new WP_Error( 'metaforge_gemini_no_key', 'Google Gemini API key is missing. Please enter your API key in MetaForge AI Settings or use Settings > Connectors.' ); } // Pass API key via URL query parameter (standard REST API) and x-goog-api-key header // to guarantee authorization even if the host server strips custom request headers. // phpcs:ignore PluginCheck.CodeAnalysis.AIProvider.DirectIntegration -- Direct integration required for user-supplied Gemini keys and WP < 7.0 compatibility. $url = add_query_arg( 'key', rawurlencode( $decrypted_key ), 'https://generativelanguage.googleapis.com/v1beta/models/' . $model_name . ':generateContent' ); $body = array( 'system_instruction' => array( 'parts' => array( array( 'text' => $system_prompt ) ), ), 'contents' => array( array( 'role' => 'user', 'parts' => array( array( 'text' => $user_prompt ) ), ), ), 'generationConfig' => array( 'temperature' => 0.7, 'maxOutputTokens' => 8192, ), ); if ( $json_mode ) { $body['generationConfig']['responseMimeType'] = 'application/json'; } $args = array( 'body' => wp_json_encode( $body ), 'headers' => array( 'Content-Type' => 'application/json', 'x-goog-api-key' => $decrypted_key, ), 'timeout' => 60, ); $response = wp_remote_post( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $response_body = wp_remote_retrieve_body( $response ); $data = json_decode( $response_body, true ); if ( isset( $data['error'] ) ) { $err = $data['error']; $message = $err['message'] ?? 'Unknown Gemini API error.'; $code = $err['code'] ?? 0; $retry_seconds = 0; if ( ! empty( $err['details'] ) && is_array( $err['details'] ) ) { foreach ( $err['details'] as $detail ) { if ( isset( $detail['retryDelay'] ) ) { $retry_seconds = (int) floatval( $detail['retryDelay'] ); break; } if ( isset( $detail['metadata']['retryDelay'] ) ) { $retry_seconds = (int) floatval( $detail['metadata']['retryDelay'] ); break; } } } if ( ! $retry_seconds && preg_match( '/retry in ([\d.]+)s/i', $message, $m ) ) { $retry_seconds = (int) ceil( floatval( $m[1] ) ); } $is_quota = ( $code === 429 || strpos( $message, 'quota' ) !== false || strpos( $message, 'RESOURCE_EXHAUSTED' ) !== false ); if ( $is_quota ) { $hint = 'Your Gemini API project has exhausted its quota. '; $hint .= 'To fix: enable billing at console.cloud.google.com/billing '; $hint .= 'or generate a new key at aistudio.google.com.'; $hint .= '

Google API Error Info: ' . esc_html( $message ); return new WP_Error( 'metaforge_gemini_quota', $hint, array( 'retry_after' => $retry_seconds, 'raw' => $message, ) ); } if ( strpos( $message, 'unregistered callers' ) !== false ) { $hint = 'Gemini API Key Authentication Failed: Google rejected the caller as unregistered.

' . '👉 To fix: Ensure you copied your complete Gemini API key from aistudio.google.com and saved it in MetaForge AI Settings.'; return new WP_Error( 'metaforge_gemini_unregistered', $hint ); } return new WP_Error( 'metaforge_gemini_error', $message, array( 'code' => $code ) ); } return $data['candidates'][0]['content']['parts'][0]['text'] ?? ''; } }