do_clear_local_backend_cache(); return; } } else { $this->do_clear_local_backend_cache(); return; } } } } /** * Actually clear the cache and log the action. * * @since 0.2.0 */ private function do_clear_local_backend_cache() { if (!class_exists("WP_Agentic_Writer_Provider_Manager")) { return; } $count = WP_Agentic_Writer_Provider_Manager::clear_connection_test_cache(); if (defined("WP_DEBUG") && WP_DEBUG) { error_log( "WPAW Settings: Local backend settings changed. Cleared " . $count . " connection test cache entries.", ); } } /** * Enqueue scripts for settings page. * * @since 0.2.0 * @param string $hook Current admin page hook. */ public function enqueue_scripts($hook) { if ("settings_page_wp-agentic-writer-settings" !== $hook) { return; } $settings_css_path = WP_AGENTIC_WRITER_DIR . "views/settings-v2/style.css"; wp_enqueue_style( "wpaw-settings-v2-stitch", WP_AGENTIC_WRITER_URL . "views/settings-v2/style.css", [], file_exists($settings_css_path) ? filemtime($settings_css_path) : WP_AGENTIC_WRITER_VERSION, ); wp_enqueue_style( "wpaw-select2", WP_AGENTIC_WRITER_URL . "assets/vendor/select2/select2.min.css", [], "4.1.0-rc.0", ); wp_enqueue_script( "wpaw-select2", WP_AGENTIC_WRITER_URL . "assets/vendor/select2/select2.min.js", ["jquery"], "4.1.0-rc.0", true, ); wp_enqueue_script( "wp-agentic-writer-settings-v2", WP_AGENTIC_WRITER_URL . "assets/js/settings-v2-stitch.js", ["jquery", "wpaw-select2"], WP_AGENTIC_WRITER_VERSION, true, ); $settings = get_option("wp_agentic_writer_settings", []); wp_localize_script("wp-agentic-writer-settings-v2", "wpawSettingsV2", [ "ajaxUrl" => admin_url("admin-ajax.php"), "nonce" => wp_create_nonce("wpaw_settings"), "models" => $this->get_models_for_select(), "currentModels" => [ "planning" => $settings["planning_model"] ?? WPAW_Model_Registry::get_default_model("planning"), "writing" => $settings["writing_model"] ?? ($settings["execution_model"] ?? WPAW_Model_Registry::get_default_model("writing")), "execution" => $settings["writing_model"] ?? ($settings["execution_model"] ?? WPAW_Model_Registry::get_default_model("writing")), "clarity" => $settings["clarity_model"] ?? WPAW_Model_Registry::get_default_model("clarity"), "refinement" => $settings["refinement_model"] ?? WPAW_Model_Registry::get_default_model("refinement"), "chat" => $settings["chat_model"] ?? WPAW_Model_Registry::get_default_model("chat"), "image" => $settings["image_model"] ?? WPAW_Model_Registry::get_default_model("image"), ], "presets" => $this->get_model_presets(), "i18n" => [ "refreshing" => __("Refreshing...", "wp-agentic-writer"), "refreshModels" => __("Refresh Models", "wp-agentic-writer"), "saveSuccess" => __( "Settings saved successfully!", "wp-agentic-writer", ), "saveError" => __( "Error saving settings.", "wp-agentic-writer", ), "confirmReset" => __( "Are you sure you want to reset all settings to defaults?", "wp-agentic-writer", ), "loading" => __("Loading...", "wp-agentic-writer"), "noResults" => __("No models found", "wp-agentic-writer"), "searchPlaceholder" => __( "Search models...", "wp-agentic-writer", ), ], ]); } /** * Get curated model presets (centralized source). * * These are intentional product decisions for different budget tiers. * Model IDs may differ from registry defaults to balance cost/quality. * * @since 0.2.0 * @return array Curated model presets. */ public function get_model_presets() { return [ "budget" => [ "chat" => "google/gemini-2.5-flash", "clarity" => "google/gemini-2.5-flash", "planning" => "google/gemini-2.5-flash", "writing" => "mistralai/mistral-small-creative", "refinement" => "google/gemini-2.5-flash", "image" => "openai/gpt-4o", ], "balanced" => [ "chat" => "google/gemini-2.5-flash", "clarity" => "google/gemini-2.5-flash", "planning" => "google/gemini-2.5-flash", "writing" => "anthropic/claude-sonnet-4", "refinement" => "anthropic/claude-sonnet-4", "image" => "openai/gpt-4o", ], "premium" => [ "chat" => "google/gemini-3-flash-preview", "clarity" => "anthropic/claude-sonnet-4", "planning" => "google/gemini-3-flash-preview", "writing" => "openai/gpt-4.1", "refinement" => "openai/gpt-4.1", "image" => "openai/gpt-4o", ], ]; } /** * Get models for select dropdowns. * * @since 0.2.0 * @return array Models grouped by category. */ public function get_models_for_select() { $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $models = $provider->get_cached_models(); if (is_wp_error($models)) { return $this->get_fallback_models(); } $transformed = $this->transform_models_for_js($models); // Debug logging if (defined("WP_DEBUG") && WP_DEBUG) { $custom_models = get_option("wp_agentic_writer_custom_models", []); error_log( "WPAW get_models_for_select: custom_models in DB = " . wp_json_encode($custom_models), ); error_log( "WPAW get_models_for_select: image models count = " . count($transformed["image"]["all"] ?? []), ); } return $transformed; } /** * Format model name from ID. * * @since 0.2.0 * @param string $model_id Model ID. * @return string Formatted model name. */ private function format_model_name($model_id) { // Remove provider prefix $parts = explode("/", $model_id); $name = end($parts); // Remove :free suffix $name = preg_replace('/:free$/i', "", $name); // Convert hyphens and underscores to spaces $name = str_replace(["-", "_"], " ", $name); // Capitalize words $name = ucwords($name); // Add provider prefix back if (count($parts) > 1) { $provider = ucfirst($parts[0]); $name = $provider . ": " . $name; } return $name; } /** * Get fallback models when API fails. * * @since 0.2.0 * @return array Fallback model structure. */ private function get_fallback_models() { return [ "planning" => [ "recommended" => [ [ "id" => WPAW_Model_Registry::get_default_model( "planning", ), "name" => WPAW_Model_Registry::get_model_display_name( WPAW_Model_Registry::get_default_model("planning"), ), ], ], "all" => [ [ "id" => WPAW_Model_Registry::get_default_model( "planning", ), "name" => WPAW_Model_Registry::get_model_display_name( WPAW_Model_Registry::get_default_model("planning"), ), ], ], ], "execution" => [ "recommended" => [ [ "id" => WPAW_Model_Registry::get_fallback_model( "execution", ), "name" => WPAW_Model_Registry::get_model_display_name( WPAW_Model_Registry::get_fallback_model( "execution", ), ), ], ], "all" => [ [ "id" => WPAW_Model_Registry::get_fallback_model( "execution", ), "name" => WPAW_Model_Registry::get_model_display_name( WPAW_Model_Registry::get_fallback_model( "execution", ), ), ], ], ], "image" => [ "recommended" => [ [ "id" => WPAW_Model_Registry::get_default_model("image"), "name" => WPAW_Model_Registry::get_model_display_name( WPAW_Model_Registry::get_default_model("image"), ), ], ], "all" => [ [ "id" => WPAW_Model_Registry::get_default_model("image"), "name" => WPAW_Model_Registry::get_model_display_name( WPAW_Model_Registry::get_default_model("image"), ), ], ], ], ]; } /** * Transform models structure for JavaScript consumption. * * @since 0.2.0 * @param array $models Models from provider. * @return array Transformed models. */ private function transform_models_for_js($models) { // Handle flat model list from OpenRouter if ( !empty($models) && array_keys($models) === range(0, count($models) - 1) ) { $settings = get_option("wp_agentic_writer_settings", []); $planning_id = $settings["planning_model"] ?? WPAW_Model_Registry::get_default_model("planning"); $execution_id = $settings["execution_model"] ?? WPAW_Model_Registry::get_default_model("execution"); $image_id = $settings["image_model"] ?? WPAW_Model_Registry::get_default_model("image"); $text_models = []; $image_models = []; // Categorize models using OpenRouter's output_modalities field foreach ($models as $model) { if (empty($model["id"])) { continue; } $prompt_price = isset($model["pricing"]["prompt"]) ? (float) $model["pricing"]["prompt"] : 0; $completion_price = isset($model["pricing"]["completion"]) ? (float) $model["pricing"]["completion"] : 0; $image_price = isset($model["pricing"]["image"]) ? (float) $model["pricing"]["image"] : 0; $model_data = [ "id" => $model["id"], "name" => $model["name"] ?? $model["id"], "is_free" => $prompt_price <= 0.0 && $completion_price <= 0.0 && $image_price <= 0.0, "pricing" => [ "prompt" => $prompt_price, "completion" => $completion_price, "image" => $image_price, ], ]; // Use OpenRouter's output_modalities to categorize - trust OpenRouter's classification $output_modalities = $model["architecture"]["output_modalities"] ?? []; // Image generation models have 'image' in output_modalities if (in_array("image", $output_modalities, true)) { $image_models[] = $model_data; } // Text models have 'text' in output_modalities (most models) if (in_array("text", $output_modalities, true)) { $text_models[] = $model_data; } } $chat_id = $settings["chat_model"] ?? WPAW_Model_Registry::get_default_model("chat"); $clarity_id = $settings["clarity_model"] ?? WPAW_Model_Registry::get_default_model("clarity"); $refinement_id = $settings["refinement_model"] ?? WPAW_Model_Registry::get_default_model("refinement"); $writing_id = $settings["writing_model"] ?? ($settings["execution_model"] ?? WPAW_Model_Registry::get_default_model("writing")); // Add currently selected models to text_models if not already present $current_model_ids = [ $planning_id, $execution_id, $chat_id, $clarity_id, $refinement_id, $writing_id, ]; foreach ($current_model_ids as $model_id) { $found = false; foreach ($text_models as $tm) { if ($tm["id"] === $model_id) { $found = true; break; } } if (!$found && !empty($model_id)) { $text_models[] = [ "id" => $model_id, "name" => $this->format_model_name($model_id), "is_free" => false, "pricing" => [ "prompt" => 0, "completion" => 0, "image" => 0, ], ]; } } // Add currently selected image model to image_models if not already present if (!empty($image_id)) { $found = false; foreach ($image_models as $im) { if ($im["id"] === $image_id) { $found = true; break; } } if (!$found) { $image_models[] = [ "id" => $image_id, "name" => $this->format_model_name($image_id), "is_free" => false, "pricing" => [ "prompt" => 0, "completion" => 0, "image" => 0, ], ]; } } // Add user's custom models (not listed in API but callable by ID) $custom_models = get_option("wp_agentic_writer_custom_models", []); foreach ($custom_models as $custom) { if (empty($custom["id"])) { continue; } $custom_model_data = [ "id" => $custom["id"], "name" => !empty($custom["name"]) ? $custom["name"] : $this->format_model_name($custom["id"]), "is_free" => false, "is_custom" => true, "pricing" => [ "prompt" => 0, "completion" => 0, "image" => 0, ], ]; $type = $custom["type"] ?? "text"; if ("image" === $type) { $image_models[] = $custom_model_data; } else { $text_models[] = $custom_model_data; } } // Now create find_model closure after all models are added $find_model = function ($model_id) use ( $text_models, $image_models, ) { foreach (array_merge($text_models, $image_models) as $model) { if ($model["id"] === $model_id) { return $model; } } // If model not found, create a fallback entry if (!empty($model_id)) { return [ "id" => $model_id, "name" => $this->format_model_name($model_id), "is_free" => false, "pricing" => [ "prompt" => 0, "completion" => 0, "image" => 0, ], ]; } return null; }; return [ "planning" => [ "recommended" => array_filter([$find_model($planning_id)]), "all" => $text_models, ], "execution" => [ "recommended" => array_filter([$find_model($execution_id)]), "all" => $text_models, ], "chat" => [ "recommended" => array_filter([$find_model($chat_id)]), "all" => $text_models, ], "image" => [ "recommended" => array_filter([$find_model($image_id)]), "all" => $image_models, ], ]; } $transformed = []; foreach ($models as $type => $categories) { if (!isset($transformed[$type])) { $transformed[$type] = [ "recommended" => [], "all" => [], ]; } // Combine free and paid into 'all' array $all_models = array_merge( $categories["free"] ?? [], $categories["paid"] ?? [], ); // Remove duplicates $recommended_ids = []; foreach ($categories["recommended"] ?? [] as $model) { $transformed[$type]["recommended"][] = $model; $recommended_ids[$model["id"]] = true; } // Add all models, avoiding duplicates with recommended foreach ($all_models as $model) { if (!isset($recommended_ids[$model["id"]])) { $transformed[$type]["all"][] = $model; } } } return $transformed; } /** * AJAX handler for refreshing models. * * @since 0.2.0 */ public function ajax_refresh_models() { if (!check_ajax_referer("wpaw_settings", "nonce", false)) { wp_send_json_error(["message" => "Invalid nonce"]); return; } if (!current_user_can("manage_options")) { wp_send_json_error(["message" => "Permission denied"]); } $settings = get_option("wp_agentic_writer_settings", []); $posted_api_key = isset($_POST["api_key"]) ? trim(sanitize_text_field(wp_unslash($_POST["api_key"]))) : ""; $api_key = !empty($posted_api_key) ? $posted_api_key : $settings["openrouter_api_key"] ?? ""; $provider = !empty($posted_api_key) ? WP_Agentic_Writer_OpenRouter_Provider::for_api_key($api_key) : WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $models = $provider->fetch_and_cache_models(true); if (is_wp_error($models)) { wp_send_json_error(["message" => $models->get_error_message()]); } $transformed = $this->transform_models_for_js($models); wp_send_json_success([ "models" => $transformed, "message" => __( "Models refreshed successfully!", "wp-agentic-writer", ), ]); } /** * AJAX handler for saving a custom model. * * @since 0.2.0 */ public function ajax_save_custom_model() { if (!check_ajax_referer("wpaw_settings", "nonce", false)) { wp_send_json_error(["message" => "Invalid nonce"]); return; } if (!current_user_can("manage_options")) { wp_send_json_error(["message" => "Permission denied"]); return; } $model_id = isset($_POST["model_id"]) ? sanitize_text_field($_POST["model_id"]) : ""; $model_name = isset($_POST["model_name"]) ? sanitize_text_field($_POST["model_name"]) : ""; $model_type = isset($_POST["model_type"]) ? sanitize_text_field($_POST["model_type"]) : "text"; if (empty($model_id)) { wp_send_json_error(["message" => "Model ID is required"]); return; } // Use separate option for custom models $custom_models = get_option("wp_agentic_writer_custom_models", []); // Check if model already exists, update it $found = false; foreach ($custom_models as $index => $cm) { if ($cm["id"] === $model_id) { $custom_models[$index] = [ "id" => $model_id, "name" => $model_name, "type" => $model_type, ]; $found = true; break; } } // Add new model if not found if (!$found) { $custom_models[] = [ "id" => $model_id, "name" => $model_name, "type" => $model_type, ]; } $saved = update_option( "wp_agentic_writer_custom_models", array_values($custom_models), ); // Get fresh combined models for Select2 $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $models = $provider->get_cached_models(); if (is_wp_error($models)) { $models = []; } $transformed = $this->transform_models_for_js($models); wp_send_json_success([ "message" => __("Custom model saved!", "wp-agentic-writer"), "models" => $transformed, ]); } /** * AJAX handler for deleting a custom model. * * @since 0.2.0 */ public function ajax_delete_custom_model() { if (!check_ajax_referer("wpaw_settings", "nonce", false)) { wp_send_json_error(["message" => "Invalid nonce"]); return; } if (!current_user_can("manage_options")) { wp_send_json_error(["message" => "Permission denied"]); return; } $model_id = isset($_POST["model_id"]) ? sanitize_text_field($_POST["model_id"]) : ""; if (empty($model_id)) { wp_send_json_error(["message" => "Model ID is required"]); return; } // Use separate option for custom models $custom_models = get_option("wp_agentic_writer_custom_models", []); // Remove the model $custom_models = array_filter($custom_models, function ($cm) use ( $model_id, ) { return $cm["id"] !== $model_id; }); update_option( "wp_agentic_writer_custom_models", array_values($custom_models), ); // Get fresh combined models for Select2 $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $models = $provider->get_cached_models(); if (is_wp_error($models)) { $models = []; } $transformed = $this->transform_models_for_js($models); wp_send_json_success([ "message" => __("Custom model deleted!", "wp-agentic-writer"), "models" => $transformed, ]); } /** * AJAX handler for getting cost log data (server-side pagination). * * @since 0.2.0 */ public function ajax_get_cost_log_data() { if (!check_ajax_referer("wpaw_settings", "nonce", false)) { wp_send_json_error(["message" => "Invalid nonce"]); return; } if (!current_user_can("manage_options")) { wp_send_json_error(["message" => "Permission denied"]); return; } global $wpdb; $table_name = $wpdb->prefix . "wpaw_cost_tracking"; // Check if table exists $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") === $table_name; if (!$table_exists) { wp_send_json_success([ "records" => [], "total_items" => 0, "total_pages" => 0, "current_page" => 1, "per_page" => 25, "stats" => [ "all_time" => "0.0000", "monthly" => "0.0000", "today" => "0.0000", "avg_per_post" => "0.0000", "action_summary" => [], ], "filters" => [ "models" => [], "types" => [], ], ]); return; } // Get parameters $page = isset($_POST["page"]) ? max(1, intval($_POST["page"])) : 1; $per_page = isset($_POST["per_page"]) ? min(100, max(10, intval($_POST["per_page"]))) : 25; $offset = ($page - 1) * $per_page; // Filters $filter_post = isset($_POST["filter_post"]) ? intval($_POST["filter_post"]) : 0; $filter_model = isset($_POST["filter_model"]) ? sanitize_text_field($_POST["filter_model"]) : ""; $filter_type = isset($_POST["filter_type"]) ? sanitize_text_field($_POST["filter_type"]) : ""; $filter_date_from = isset($_POST["filter_date_from"]) ? sanitize_text_field($_POST["filter_date_from"]) : ""; $filter_date_to = isset($_POST["filter_date_to"]) ? sanitize_text_field($_POST["filter_date_to"]) : ""; // Build WHERE clause (OpenRouter-only for this OpenRouter cost log screen). $where = ["provider = 'openrouter'"]; if ($filter_post > 0) { $where[] = $wpdb->prepare("post_id = %d", $filter_post); } if (!empty($filter_model)) { $where[] = $wpdb->prepare("model = %s", $filter_model); } if (!empty($filter_type)) { $where[] = $wpdb->prepare("action = %s", $filter_type); } if (!empty($filter_date_from)) { $where[] = $wpdb->prepare( "DATE(created_at) >= %s", $filter_date_from, ); } if (!empty($filter_date_to)) { $where[] = $wpdb->prepare( "DATE(created_at) <= %s", $filter_date_to, ); } $where_clause = implode(" AND ", $where); // Get total count of distinct posts $total_items = $wpdb->get_var( "SELECT COUNT(DISTINCT post_id) FROM {$table_name} WHERE {$where_clause}", ); $total_pages = ceil($total_items / $per_page); // Optimized: Get grouped records with aggregation in SQL. // This pushes grouping and ordering to the database instead of PHP. $grouped_records_sql = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, SUM(cost) as total_cost, COUNT(*) as call_count, MAX(created_at) as last_call FROM {$table_name} WHERE {$where_clause} GROUP BY post_id ORDER BY post_id DESC LIMIT %d OFFSET %d", $per_page, $offset, ), ARRAY_A, ); // Build grouped records with post details $formatted_records = []; $post_ids = []; foreach ($grouped_records_sql as $row) { $post_id = (int) $row["post_id"]; $post_ids[] = $post_id; if ($post_id > 0) { $post_title = get_the_title($post_id); if (!$post_title) { $post_title = sprintf( __("[Removed Post #%d]", "wp-agentic-writer"), $post_id, ); $post_link = ""; } else { $post_link = get_edit_post_link($post_id, "raw"); } } else { $post_title = __("System/Other", "wp-agentic-writer"); $post_link = ""; } $formatted_records[] = [ "post_id" => $post_id, "post_title" => $post_title, "post_link" => $post_link, "total_cost" => number_format((float) $row["total_cost"], 4), "call_count" => (int) $row["call_count"], "last_call" => date_i18n( "Y-m-d H:i:s", strtotime($row["last_call"]), ), "details" => [], // Lazy-loaded on expand ]; } // Load detail rows for visible posts. // This keeps expand/collapse usable without requiring a second endpoint. if (!empty($post_ids)) { $placeholders = implode(",", array_fill(0, count($post_ids), "%d")); $details_sql = $wpdb->prepare( "SELECT post_id, created_at, model, action, input_tokens, output_tokens, cost FROM {$table_name} WHERE provider = 'openrouter' AND post_id IN ({$placeholders}) ORDER BY created_at DESC", ...$post_ids, ); $detail_rows = $wpdb->get_results($details_sql, ARRAY_A); $image_variants_table = $wpdb->prefix . "wpaw_images_variants"; $image_variants_table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$image_variants_table}'") === $image_variants_table; if ($image_variants_table_exists) { // Find posts that already have image_generation in wpaw_cost_tracking // to avoid duplicates from the variants table. $posts_with_tracked_images = []; foreach ($detail_rows as $existing) { if (($existing["action"] ?? "") === "image_generation") { $posts_with_tracked_images[ (int) ($existing["post_id"] ?? 0) ] = true; } } $variant_post_ids = array_filter($post_ids, function ( $pid, ) use ($posts_with_tracked_images) { return empty($posts_with_tracked_images[(int) $pid]); }); if (!empty($variant_post_ids)) { $variant_placeholders = implode( ",", array_fill(0, count($variant_post_ids), "%d"), ); $image_details_sql = $wpdb->prepare( "SELECT post_id, created_at, image_model_used AS model, cost FROM {$image_variants_table} WHERE post_id IN ({$variant_placeholders}) AND cost IS NOT NULL AND cost > 0 ORDER BY created_at DESC", ...$variant_post_ids, ); $image_detail_rows = $wpdb->get_results( $image_details_sql, ARRAY_A, ); foreach ($image_detail_rows as $image_detail_row) { $detail_rows[] = [ "post_id" => (int) ($image_detail_row["post_id"] ?? 0), "created_at" => $image_detail_row["created_at"] ?? "", "model" => $image_detail_row["model"] ?? "", "action" => "image_generation", "input_tokens" => 0, "output_tokens" => 0, "cost" => $image_detail_row["cost"] ?? 0, ]; } } } $detail_map = []; foreach ($detail_rows as $detail_row) { $pid = (int) ($detail_row["post_id"] ?? 0); if (!isset($detail_map[$pid])) { $detail_map[$pid] = []; } $detail_map[$pid][] = [ "created_at" => date_i18n( "Y-m-d H:i:s", strtotime($detail_row["created_at"]), ), "model" => (string) ($detail_row["model"] ?? ""), "action" => (string) ($detail_row["action"] ?? ""), "input_tokens" => (int) ($detail_row["input_tokens"] ?? 0), "output_tokens" => (int) ($detail_row["output_tokens"] ?? 0), "cost" => number_format( (float) ($detail_row["cost"] ?? 0), 4, ), ]; } foreach ($formatted_records as $idx => $formatted_record) { $pid = (int) ($formatted_record["post_id"] ?? 0); $formatted_records[$idx]["details"] = $detail_map[$pid] ?? []; $formatted_records[$idx]["details_total"] = count( $formatted_records[$idx]["details"], ); } } // Get summary stats (all-time aggregation in SQL) $total_all_time = $wpdb->get_var( "SELECT COALESCE(SUM(cost), 0) FROM {$table_name} WHERE provider = 'openrouter'", ); $month_start = date("Y-m-01 00:00:00"); $monthly_total = $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(cost), 0) FROM {$table_name} WHERE provider = 'openrouter' AND created_at >= %s", $month_start, ), ); $today_total = $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(cost), 0) FROM {$table_name} WHERE provider = 'openrouter' AND DATE(created_at) = %s", current_time("Y-m-d"), ), ); $total_posts = $wpdb->get_var( "SELECT COUNT(DISTINCT post_id) FROM {$table_name} WHERE provider = 'openrouter' AND post_id > 0", ); $avg_per_post = $total_posts > 0 ? $total_all_time / $total_posts : 0; $action_summary_rows = $wpdb->get_results( "SELECT action, COUNT(*) AS calls, COALESCE(SUM(cost), 0) AS total_cost, COALESCE(AVG(cost), 0) AS avg_cost FROM {$table_name} WHERE provider = 'openrouter' GROUP BY action ORDER BY total_cost DESC", ARRAY_A, ); $action_summary = []; foreach ($action_summary_rows as $row) { $action_summary[] = [ "action" => (string) ($row["action"] ?? ""), "calls" => (int) ($row["calls"] ?? 0), "total" => number_format((float) ($row["total_cost"] ?? 0), 4), "average" => number_format((float) ($row["avg_cost"] ?? 0), 4), ]; } // Get filter options (distinct values from DB) $models = $wpdb->get_col( "SELECT DISTINCT model FROM {$table_name} WHERE provider = 'openrouter' ORDER BY model LIMIT 100", ); $types = $wpdb->get_col( "SELECT DISTINCT action FROM {$table_name} WHERE provider = 'openrouter' ORDER BY action", ); wp_send_json_success([ "records" => $formatted_records, "total_items" => intval($total_items), "total_pages" => intval($total_pages), "current_page" => $page, "per_page" => $per_page, "stats" => [ "all_time" => number_format((float) $total_all_time, 4), "monthly" => number_format((float) $monthly_total, 4), "today" => number_format((float) $today_total, 4), "avg_per_post" => number_format((float) $avg_per_post, 4), "action_summary" => $action_summary, ], "filters" => [ "models" => $models, "types" => $types, ], ]); } /** * AJAX handler for header statistics. * * @since 0.2.0 */ public function ajax_get_header_stats() { if (!check_ajax_referer("wpaw_settings", "nonce", false)) { wp_send_json_error(["message" => "Invalid nonce"]); return; } if (!current_user_can("manage_options")) { wp_send_json_error(["message" => "Permission denied"]); return; } global $wpdb; $table_name = $wpdb->prefix . "wpaw_cost_tracking"; // Check if table exists $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") === $table_name; if (!$table_exists) { wp_send_json_success([ "articles" => 0, "total_cost" => "0.00", "api_status" => "Not configured", "api_online" => false, "last_updated" => "Never", ]); return; } // Get total articles $total_articles = $wpdb->get_var( "SELECT COUNT(DISTINCT post_id) FROM {$table_name} WHERE post_id > 0", ); // Get total cost $total_cost = $wpdb->get_var("SELECT SUM(cost) FROM {$table_name}"); // Check API status $settings = get_option("wp_agentic_writer_settings", []); $api_key = $settings["openrouter_api_key"] ?? ""; $api_online = !empty($api_key); // Get last activity $last_activity = $wpdb->get_var( "SELECT created_at FROM {$table_name} ORDER BY created_at DESC LIMIT 1", ); $last_updated = $last_activity ? human_time_diff( strtotime($last_activity), current_time("timestamp"), ) . " ago" : "Never"; wp_send_json_success([ "articles" => intval($total_articles), "total_cost" => number_format((float) $total_cost, 2), "api_status" => $api_online ? "Online" : "Not configured", "api_online" => $api_online, "last_updated" => $last_updated, ]); } /** * AJAX handler for debugging models. * * @since 0.2.0 */ public function ajax_debug_models() { if (!check_ajax_referer("wpaw_settings", "nonce", false)) { wp_send_json_error(["message" => "Invalid nonce"]); return; } if (!current_user_can("manage_options")) { wp_send_json_error(["message" => "Permission denied"]); return; } $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $models = $provider->get_cached_models(); if (is_wp_error($models)) { wp_send_json_error(["message" => $models->get_error_message()]); return; } // Check for specific models $check_models = [ "deepseek/deepseek-chat-v3-0324", "anthropic/claude-sonnet-4", ]; $found_models = []; $missing_models = []; foreach ($check_models as $check_id) { $found = false; foreach ($models as $model) { if (isset($model["id"]) && $model["id"] === $check_id) { $found = true; $found_models[] = [ "id" => $model["id"], "name" => $model["name"] ?? "N/A", ]; break; } } if (!$found) { $missing_models[] = $check_id; } } wp_send_json_success([ "total_models" => count($models), "found_models" => $found_models, "missing_models" => $missing_models, "sample_models" => array_slice( array_map(function ($m) { return [ "id" => $m["id"] ?? "N/A", "name" => $m["name"] ?? "N/A", ]; }, $models), 0, 10, ), ]); } /** * AJAX handler for testing API connection. * * @since 0.2.0 */ public function ajax_test_api_connection() { if (!check_ajax_referer("wpaw_settings", "nonce", false)) { wp_send_json_error(["message" => "Invalid nonce"]); return; } if (!current_user_can("manage_options")) { wp_send_json_error(["message" => "Permission denied"]); return; } $settings = get_option("wp_agentic_writer_settings", []); $posted_api_key = isset($_POST["api_key"]) ? trim(sanitize_text_field(wp_unslash($_POST["api_key"]))) : ""; $api_key = !empty($posted_api_key) ? $posted_api_key : $settings["openrouter_api_key"] ?? ""; if (empty($api_key)) { wp_send_json_error(["message" => "API key is not configured"]); return; } // Test API connection by making a simple request $response = wp_remote_get( "https://openrouter.ai/api/v1/models?output_modalities=all", [ "headers" => [ "Authorization" => "Bearer " . $api_key, "HTTP-Referer" => home_url(), ], "timeout" => 10, ], ); if (is_wp_error($response)) { wp_send_json_error([ "message" => "Connection failed: " . $response->get_error_message(), ]); return; } $status_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); if (200 === $status_code) { $data = json_decode($body, true); if (isset($data["data"]) && is_array($data["data"])) { wp_send_json_success([ "message" => "API connection successful!", "models_count" => count($data["data"]), ]); return; } } // Handle error responses if (401 === $status_code) { wp_send_json_error(["message" => "Invalid API key"]); return; } if (403 === $status_code) { wp_send_json_error([ "message" => "Access forbidden - check your API key permissions", ]); return; } wp_send_json_error([ "message" => "API connection failed with status code: " . $status_code, ]); } /** * Add settings page to admin menu. * * @since 0.2.0 */ public function add_settings_page() { add_options_page( __("WP Agentic Writer", "wp-agentic-writer"), __("Agentic Writer", "wp-agentic-writer"), "manage_options", "wp-agentic-writer-settings", [$this, "render_settings_page"], ); } /** * Register settings. * * @since 0.2.0 */ public function register_settings() { register_setting( "wp_agentic_writer_settings", "wp_agentic_writer_settings", [ "sanitize_callback" => [$this, "sanitize_settings"], ], ); } /** * Sanitize settings. * * @since 0.2.0 * @param array $input Settings input. * @return array Sanitized settings. */ public function sanitize_settings($input) { $sanitized = []; // Sanitize API keys (allow empty values to clear them) if (isset($input["openrouter_api_key"])) { $sanitized["openrouter_api_key"] = trim( $input["openrouter_api_key"], ); } if (isset($input["brave_search_api_key"])) { $sanitized["brave_search_api_key"] = trim( $input["brave_search_api_key"], ); } if (isset($input["custom_search_url"])) { $sanitized["custom_search_url"] = esc_url_raw( trim($input["custom_search_url"]), ); } // Sanitize model names (6 models) - using model registry for defaults $sanitized["chat_model"] = sanitize_text_field( $input["chat_model"] ?? WPAW_Model_Registry::get_default_model("chat"), ); $sanitized["clarity_model"] = sanitize_text_field( $input["clarity_model"] ?? WPAW_Model_Registry::get_default_model("clarity"), ); $sanitized["planning_model"] = sanitize_text_field( $input["planning_model"] ?? WPAW_Model_Registry::get_default_model("planning"), ); $sanitized["writing_model"] = sanitize_text_field( $input["writing_model"] ?? WPAW_Model_Registry::get_default_model("writing"), ); $sanitized["refinement_model"] = sanitize_text_field( $input["refinement_model"] ?? WPAW_Model_Registry::get_default_model("refinement"), ); $sanitized["image_model"] = sanitize_text_field( $input["image_model"] ?? WPAW_Model_Registry::get_default_model("image"), ); // Legacy support: map execution_model to writing_model if ( isset($input["execution_model"]) && !isset($input["writing_model"]) ) { $sanitized["writing_model"] = sanitize_text_field( $input["execution_model"], ); } // Sanitize boolean values $sanitized["web_search_enabled"] = isset($input["web_search_enabled"]) && "1" === $input["web_search_enabled"]; $sanitized["cost_tracking_enabled"] = isset($input["cost_tracking_enabled"]) && "1" === $input["cost_tracking_enabled"]; $sanitized["enable_clarification_quiz"] = isset($input["enable_clarification_quiz"]) && "1" === $input["enable_clarification_quiz"]; $sanitized["enable_faq_schema"] = isset($input["enable_faq_schema"]) ? "1" === $input["enable_faq_schema"] : false; $sanitized["allow_openrouter_fallback"] = isset($input["allow_openrouter_fallback"]) && "1" === $input["allow_openrouter_fallback"]; $sanitized["openrouter_provider_routing_enabled"] = isset($input["openrouter_provider_routing_enabled"]) && "1" === $input["openrouter_provider_routing_enabled"]; $sanitized["openrouter_provider_only"] = isset($input["openrouter_provider_only"]) && "1" === $input["openrouter_provider_only"]; $sanitized["openrouter_allow_provider_fallbacks"] = isset($input["openrouter_allow_provider_fallbacks"]) && "1" === $input["openrouter_allow_provider_fallbacks"]; $provider_slug = isset($input["openrouter_provider_slug"]) ? sanitize_key($input["openrouter_provider_slug"]) : "auto"; $sanitized["openrouter_provider_slug"] = "" !== $provider_slug ? $provider_slug : "auto"; // Sanitize search options $sanitized["search_engine"] = in_array( $input["search_engine"] ?? "", ["auto", "native", "exa"], true, ) ? $input["search_engine"] : "auto"; $sanitized["search_depth"] = isset($input["search_depth"]) && in_array($input["search_depth"], ["low", "medium", "high"], true) ? $input["search_depth"] : "medium"; // Sanitize budget $sanitized["monthly_budget"] = floatval( $input["monthly_budget"] ?? 600, ); // Sanitize chat history limit $chat_history_limit = isset($input["chat_history_limit"]) ? absint($input["chat_history_limit"]) : 20; $sanitized["chat_history_limit"] = min($chat_history_limit, 200); // Sanitize clarification quiz settings $sanitized["clarity_confidence_threshold"] = in_array( $input["clarity_confidence_threshold"] ?? "", ["0.5", "0.6", "0.7", "0.8", "0.9"], true, ) ? $input["clarity_confidence_threshold"] : "0.6"; if ( isset($input["required_context_categories"]) && is_array($input["required_context_categories"]) ) { $valid_categories = [ "target_outcome", "target_audience", "tone", "content_depth", "expertise_level", "content_type", "pov", ]; $sanitized["required_context_categories"] = array_intersect( $input["required_context_categories"], $valid_categories, ); } else { $sanitized["required_context_categories"] = [ "target_outcome", "target_audience", "tone", "content_depth", "expertise_level", "content_type", "pov", ]; } // Sanitize preferred languages if ( isset($input["preferred_languages"]) && is_array($input["preferred_languages"]) ) { $sanitized["preferred_languages"] = array_map( "sanitize_text_field", $input["preferred_languages"], ); } else { $sanitized["preferred_languages"] = [ "auto", "English", "Indonesian", ]; } // Sanitize custom languages if ( isset($input["custom_languages"]) && is_array($input["custom_languages"]) ) { $custom_language_values = []; foreach ($input["custom_languages"] as $custom_language_value) { $custom_language_values = array_merge( $custom_language_values, array_map( "trim", explode(",", (string) $custom_language_value), ), ); } $sanitized["custom_languages"] = array_filter( array_map("sanitize_text_field", $custom_language_values), ); } else { $sanitized["custom_languages"] = []; } // Sanitize Global Context $sanitized["global_context"] = isset($input["global_context"]) ? sanitize_textarea_field(trim($input["global_context"])) : ""; // Sanitize Custom Endpoint settings (Fix for settings wiping out) if (isset($input["local_backend_url"])) { $sanitized["local_backend_url"] = esc_url_raw( trim($input["local_backend_url"]), ); } if (isset($input["local_backend_key"])) { $sanitized["local_backend_key"] = sanitize_text_field( trim($input["local_backend_key"]), ); } if (isset($input["local_backend_image_url"])) { $sanitized["local_backend_image_url"] = esc_url_raw( trim($input["local_backend_image_url"]), ); } if (isset($input["local_backend_image_key"])) { $sanitized["local_backend_image_key"] = sanitize_text_field( trim($input["local_backend_image_key"]), ); } if (isset($input["local_backend_model"])) { $sanitized["local_backend_model"] = sanitize_text_field( trim($input["local_backend_model"]), ); } $sanitized["local_backend_enabled"] = isset($input["local_backend_enabled"]) && "1" === $input["local_backend_enabled"]; $sanitized["local_backend_image_enabled"] = isset($input["local_backend_image_enabled"]) && "1" === $input["local_backend_image_enabled"]; // Per-task model overrides for custom endpoint if ( isset($input["local_backend_models"]) && is_array($input["local_backend_models"]) ) { $sanitized_models = []; $allowed_model_tasks = [ "chat", "clarity", "planning", "writing", "refinement", "image", ]; foreach ($input["local_backend_models"] as $task => $model_code) { $task = sanitize_text_field($task); if (in_array($task, $allowed_model_tasks, true)) { $sanitized_models[$task] = sanitize_text_field( trim($model_code), ); } } $sanitized["local_backend_models"] = $sanitized_models; } // Sanitize MEMANTO settings. $sanitized["memanto_enabled"] = isset($input["memanto_enabled"]) && "1" === $input["memanto_enabled"]; $sanitized["memanto_url"] = isset($input["memanto_url"]) ? esc_url_raw(trim($input["memanto_url"])) : ""; $sanitized["memanto_license_key"] = isset($input["memanto_license_key"]) ? sanitize_text_field(trim($input["memanto_license_key"])) : ""; $sanitized["memanto_moorcheh_key"] = isset( $input["memanto_moorcheh_key"], ) ? sanitize_text_field(trim($input["memanto_moorcheh_key"])) : ""; // Sanitize Task Providers Routing. // When custom endpoint is enabled, auto-build task_providers from // the per-task model codes: a non-empty model code ⇒ local_backend. if (!empty($sanitized["local_backend_enabled"])) { $sanitized_providers = []; $lb_models = $sanitized["local_backend_models"] ?? []; $all_tasks = [ "chat", "clarity", "planning", "writing", "refinement", "image", ]; foreach ($all_tasks as $task) { if ($task === "image") { $sanitized_providers[$task] = !empty( $sanitized["local_backend_image_enabled"] ) ? "local_backend" : "openrouter"; } else { $sanitized_providers[$task] = !empty($lb_models[$task]) ? "local_backend" : "openrouter"; } } $sanitized["task_providers"] = $sanitized_providers; } elseif ( isset($input["task_providers"]) && is_array($input["task_providers"]) ) { $sanitized_providers = []; $allowed_tasks = [ "chat", "clarity", "planning", "writing", "refinement", "image", ]; $allowed_providers_text = ["openrouter", "local_backend", "codex"]; foreach ($input["task_providers"] as $task => $provider) { $task = sanitize_text_field($task); $provider = sanitize_text_field($provider); if (in_array($task, $allowed_tasks, true)) { if (in_array($provider, $allowed_providers_text, true)) { $sanitized_providers[$task] = $provider; } } } $sanitized["task_providers"] = $sanitized_providers; } return $sanitized; } /** * Render settings page - main entry point. * * @since 0.2.0 */ public function render_settings_page() { $settings = get_option("wp_agentic_writer_settings", []); // Extract settings for views $view_data = $this->prepare_view_data($settings); // Include Stitch rebuild layout include WP_AGENTIC_WRITER_DIR . "views/settings-v2/layout.php"; } /** * Prepare data for view files. * * @since 0.2.0 * @param array $settings Plugin settings. * @return array View data. */ private function prepare_view_data($settings) { // Extract settings (6 models) using model registry for defaults $api_key = $settings["openrouter_api_key"] ?? ""; $brave_search_api_key = $settings["brave_search_api_key"] ?? ""; $custom_search_url = $settings["custom_search_url"] ?? ""; $chat_model = $settings["chat_model"] ?? WPAW_Model_Registry::get_default_model("chat"); $clarity_model = $settings["clarity_model"] ?? WPAW_Model_Registry::get_default_model("clarity"); $planning_model = $settings["planning_model"] ?? WPAW_Model_Registry::get_default_model("planning"); $writing_model = $settings["writing_model"] ?? ($settings["execution_model"] ?? WPAW_Model_Registry::get_default_model("writing")); $refinement_model = $settings["refinement_model"] ?? WPAW_Model_Registry::get_default_model("refinement"); $image_model = $settings["image_model"] ?? WPAW_Model_Registry::get_default_model("image"); $web_search_enabled = $settings["web_search_enabled"] ?? false; $search_engine = $settings["search_engine"] ?? "auto"; $search_depth = $settings["search_depth"] ?? "medium"; $cost_tracking_enabled = $settings["cost_tracking_enabled"] ?? true; $monthly_budget = $settings["monthly_budget"] ?? 600; $chat_history_limit = $settings["chat_history_limit"] ?? 20; $enable_clarification_quiz = $settings["enable_clarification_quiz"] ?? true; $enable_faq_schema = $settings["enable_faq_schema"] ?? false; $clarity_confidence_threshold = $settings["clarity_confidence_threshold"] ?? "0.6"; $required_context_categories = $settings[ "required_context_categories" ] ?? [ "target_outcome", "target_audience", "tone", "content_depth", "expertise_level", "content_type", "pov", ]; $preferred_languages = $settings["preferred_languages"] ?? [ "auto", "English", "Indonesian", ]; $custom_languages = $settings["custom_languages"] ?? []; $global_context = $settings["global_context"] ?? ""; $available_languages = $this->get_available_languages(); $custom_models = get_option("wp_agentic_writer_custom_models", []); // Custom Endpoint settings $local_backend_url = $settings["local_backend_url"] ?? ""; $local_backend_key = $settings["local_backend_key"] ?? ""; $local_backend_image_url = $settings["local_backend_image_url"] ?? ""; $local_backend_image_key = $settings["local_backend_image_key"] ?? ""; $local_backend_model = $settings["local_backend_model"] ?? ""; $local_backend_enabled = !empty($settings["local_backend_enabled"]); $local_backend_image_enabled = !empty( $settings["local_backend_image_enabled"] ); $local_backend_models = $settings["local_backend_models"] ?? []; // MEMANTO settings $memanto_enabled = $settings["memanto_enabled"] ?? false; $memanto_url = $settings["memanto_url"] ?? ""; $memanto_license_key = $settings["memanto_license_key"] ?? ""; $memanto_moorcheh_key = $settings["memanto_moorcheh_key"] ?? ""; $task_providers = $settings["task_providers"] ?? []; $allow_openrouter_fallback = !empty( $settings["allow_openrouter_fallback"] ); $openrouter_provider_routing_enabled = !empty( $settings["openrouter_provider_routing_enabled"] ); $openrouter_provider_slug = $settings["openrouter_provider_slug"] ?? "auto"; $openrouter_provider_only = !empty( $settings["openrouter_provider_only"] ); $openrouter_allow_provider_fallbacks = !empty( $settings["openrouter_allow_provider_fallbacks"] ); // Get cost tracking data $cost_tracker = WP_Agentic_Writer_Cost_Tracker::get_instance(); $monthly_used = $cost_tracker->get_monthly_total(); $budget_percent = $monthly_budget > 0 ? ($monthly_used / $monthly_budget) * 100 : 0; $budget_status = $budget_percent > 90 ? "danger" : ($budget_percent > 70 ? "warning" : "success"); return compact( "api_key", "brave_search_api_key", "custom_search_url", "chat_model", "clarity_model", "planning_model", "writing_model", "refinement_model", "image_model", "web_search_enabled", "search_engine", "search_depth", "cost_tracking_enabled", "monthly_budget", "chat_history_limit", "enable_clarification_quiz", "enable_faq_schema", "clarity_confidence_threshold", "required_context_categories", "preferred_languages", "custom_languages", "global_context", "available_languages", "custom_models", "monthly_used", "budget_percent", "budget_status", "local_backend_url", "local_backend_key", "local_backend_image_url", "local_backend_image_key", "local_backend_model", "local_backend_enabled", "local_backend_image_enabled", "local_backend_models", "task_providers", "allow_openrouter_fallback", "openrouter_provider_routing_enabled", "openrouter_provider_slug", "openrouter_provider_only", "openrouter_allow_provider_fallbacks", "memanto_enabled", "memanto_url", "memanto_license_key", "memanto_moorcheh_key", "settings", ); } /** * Get available languages list. * * @since 0.2.0 * @return array Available languages. */ public function get_available_languages() { return [ "auto" => "Auto-detect", "English" => "English", "Indonesian" => "Indonesian (Bahasa Indonesia)", "Javanese" => "Javanese (Basa Jawa)", "Sundanese" => "Sundanese (Basa Sunda)", "Spanish" => "Spanish (Español)", "French" => "French (Français)", "Arabic" => "Arabic (العربية)", "Chinese" => "Chinese (中文)", "Japanese" => "Japanese (日本語)", "Portuguese" => "Portuguese (Português)", "German" => "German (Deutsch)", "Hindi" => "Hindi (हिंदी)", "Korean" => "Korean (한국어)", "Vietnamese" => "Vietnamese (Tiếng Việt)", "Thai" => "Thai (ไทย)", "Tagalog" => "Tagalog", "Malay" => "Malay (Bahasa Melayu)", "Russian" => "Russian (Русский)", "Italian" => "Italian (Italiano)", "Dutch" => "Dutch (Nederlands)", "Polish" => "Polish (Polski)", "Turkish" => "Turkish (Türkçe)", "Swedish" => "Swedish (Svenska)", ]; } /** * AJAX handler: Test local backend connection * * @since 0.2.0 */ public function ajax_test_local_backend() { check_ajax_referer("wpaw_settings", "nonce"); if (!current_user_can("manage_options")) { wp_send_json_error(["message" => "Insufficient permissions"]); } $url = sanitize_text_field(wp_unslash($_POST["url"] ?? "")); $key = sanitize_text_field(wp_unslash($_POST["key"] ?? "")); $model = sanitize_text_field(wp_unslash($_POST["model"] ?? "")); if (empty($url)) { wp_send_json_error(["message" => "URL required"]); } // Temporarily create provider with these form values. $original_settings = get_option("wp_agentic_writer_settings", []); $temp_settings = $original_settings; $temp_settings["local_backend_url"] = $url; if ("" !== $key) { $temp_settings["local_backend_key"] = $key; } if ("" !== $model) { $temp_settings["local_backend_model"] = $model; } update_option("wp_agentic_writer_settings", $temp_settings); $provider = new WP_Agentic_Writer_Local_Backend_Provider(); $result = $provider->test_connection(); update_option("wp_agentic_writer_settings", $original_settings); if (is_wp_error($result)) { wp_send_json_error(["message" => $result->get_error_message()]); } wp_send_json_success($result); } /** * AJAX handler: Test MEMANTO connection. * * Uses the provided URL and key (from the form) to test connectivity * without requiring settings to be saved first. * * @since 0.3.0 */ public function ajax_test_memanto() { check_ajax_referer("wpaw_settings", "nonce"); if (!current_user_can("manage_options")) { wp_send_json_error(["message" => "Insufficient permissions"]); } $url = sanitize_text_field(wp_unslash($_POST["url"] ?? "")); $key = sanitize_text_field(wp_unslash($_POST["key"] ?? "")); if (empty($url) || empty($key)) { wp_send_json_error([ "message" => "MEMANTO URL and Moorcheh API key are required.", ]); } // Temporarily override settings so the client uses the form values. $original_settings = get_option("wp_agentic_writer_settings", []); $temp_settings = $original_settings; $temp_settings["memanto_url"] = esc_url_raw(trim($url)); $temp_settings["memanto_moorcheh_key"] = sanitize_text_field( trim($key), ); update_option("wp_agentic_writer_settings", $temp_settings); // Clear health cache so the fresh URL/key are used. delete_transient("wpaw_memanto_health"); $client = WP_Agentic_Writer_Memanto_Client::for_base_url( esc_url_raw(trim($url)), ); $result = $client->check_health_fresh(); update_option("wp_agentic_writer_settings", $original_settings); delete_transient("wpaw_memanto_health"); if ($result["healthy"]) { wp_send_json_success($result); } else { $error_msg = "Connection failed."; if (!empty($result["details"]["error"])) { $error_msg = $result["details"]["error"]; } wp_send_json_error(["message" => $error_msg, "healthy" => false]); } } }