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'] ?? '';
}
}