admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'wpaw_settings' ), 'models' => $this->get_models_for_select(), 'currentModels' => array( 'planning' => $settings['planning_model'] ?? 'google/gemini-2.0-flash-exp:free', 'writing' => $settings['writing_model'] ?? ( $settings['execution_model'] ?? 'anthropic/claude-3.5-sonnet' ), 'execution' => $settings['writing_model'] ?? ( $settings['execution_model'] ?? 'anthropic/claude-3.5-sonnet' ), 'clarity' => $settings['clarity_model'] ?? 'google/gemini-2.0-flash-exp:free', 'refinement' => $settings['refinement_model'] ?? 'anthropic/claude-3.5-sonnet', 'chat' => $settings['chat_model'] ?? 'google/gemini-2.0-flash-exp:free', 'image' => $settings['image_model'] ?? 'openai/gpt-4o', ), ) ); } /** * Get models for select dropdowns. * * @since 0.1.0 * @return array Models grouped by category. */ private function get_models_for_select() { $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $models = $provider->get_cached_models(); if ( is_wp_error( $models ) ) { // Return fallback defaults if API fails. return array( 'planning' => array( 'recommended' => array( array( 'id' => 'google/gemini-2.0-flash-exp:free', 'name' => 'Google Gemini 2.0 Flash' ), ), 'all' => array( array( 'id' => 'google/gemini-2.0-flash-exp:free', 'name' => 'Google Gemini 2.0 Flash' ), ), ), 'execution' => array( 'recommended' => array( array( 'id' => 'anthropic/claude-3.5-sonnet', 'name' => 'Anthropic Claude 3.5 Sonnet' ), ), 'all' => array( array( 'id' => 'anthropic/claude-3.5-sonnet', 'name' => 'Anthropic Claude 3.5 Sonnet' ), ), ), 'image' => array( 'recommended' => array( array( 'id' => 'openai/gpt-4o', 'name' => 'OpenAI GPT-4o' ), ), 'all' => array( array( 'id' => 'openai/gpt-4o', 'name' => 'OpenAI GPT-4o' ), ), ), ); } // Transform model structure to match what JS expects. return $this->transform_models_for_js( $models ); } /** * Transform models structure for JavaScript consumption. * * @since 0.1.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', array() ); $planning_id = $settings['planning_model'] ?? 'google/gemini-2.0-flash-exp:free'; $execution_id = $settings['execution_model'] ?? 'anthropic/claude-3.5-sonnet'; $image_id = $settings['image_model'] ?? 'openai/gpt-4o'; $text_models = array(); $image_models = array(); // Known image model prefixes $image_model_prefixes = array( 'black-forest-labs/', 'stability-ai/', 'dall-e', 'midjourney' ); 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 = array( 'id' => $model['id'], 'name' => $model['name'] ?? $model['id'], 'is_free' => $prompt_price <= 0.0 && $completion_price <= 0.0, 'pricing' => array( 'prompt' => $prompt_price, 'completion' => $completion_price, 'image' => $image_price, ), ); // Check if this is an image model $is_image_model = false; foreach ( $image_model_prefixes as $prefix ) { if ( stripos( $model['id'], $prefix ) === 0 ) { $is_image_model = true; break; } } // Also check if it has image pricing but no text pricing if ( $image_price > 0 && $prompt_price <= 0 && $completion_price <= 0 ) { $is_image_model = true; } if ( $is_image_model ) { $image_models[] = $model_data; } else { $text_models[] = $model_data; } } $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; } } return null; }; $chat_id = $settings['chat_model'] ?? 'google/gemini-2.0-flash-exp:free'; return array( 'planning' => array( 'recommended' => array_filter( array( $find_model( $planning_id ) ) ), 'all' => $text_models, ), 'execution' => array( 'recommended' => array_filter( array( $find_model( $execution_id ) ) ), 'all' => $text_models, ), 'chat' => array( 'recommended' => array_filter( array( $find_model( $chat_id ) ) ), 'all' => $text_models, ), 'image' => array( 'recommended' => array_filter( array( $find_model( $image_id ) ) ), 'all' => $image_models, ), ); } $transformed = array(); foreach ( $models as $type => $categories ) { if ( ! isset( $transformed[ $type ] ) ) { $transformed[ $type ] = array( 'recommended' => array(), 'all' => array(), ); } // Combine free and paid into 'all' array. $all_models = array_merge( $categories['free'] ?? array(), $categories['paid'] ?? array() ); // Remove duplicates (in case a model is in both recommended and all). $recommended_ids = array(); foreach ( $categories['recommended'] ?? array() 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.1.0 */ public function ajax_refresh_models() { check_ajax_referer( 'wpaw_settings', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Permission denied' ) ); } $provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance(); $models = $provider->fetch_and_cache_models( true ); if ( is_wp_error( $models ) ) { wp_send_json_error( array( 'message' => $models->get_error_message() ) ); } // Transform models for JS consumption. $transformed = $this->transform_models_for_js( $models ); wp_send_json_success( array( 'models' => $transformed, 'message' => __( 'Models refreshed successfully!', 'wp-agentic-writer' ), ) ); } /** * Add settings page to admin menu. * * @since 0.1.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', array( $this, 'render_settings_page' ) ); } /** * Register settings. * * @since 0.1.0 */ public function register_settings() { register_setting( 'wp_agentic_writer_settings', 'wp_agentic_writer_settings', array( 'sanitize_callback' => array( $this, 'sanitize_settings' ), ) ); } /** * Sanitize settings. * * @since 0.1.0 * @param array $input Settings input. * @return array Sanitized settings. */ public function sanitize_settings( $input ) { $sanitized = array(); // Sanitize API key (don't strip tags, but trim). $sanitized['openrouter_api_key'] = trim( $input['openrouter_api_key'] ?? '' ); // Sanitize model names (6 models as per model-preset-brief.md). $sanitized['chat_model'] = sanitize_text_field( $input['chat_model'] ?? 'google/gemini-2.5-flash' ); $sanitized['clarity_model'] = sanitize_text_field( $input['clarity_model'] ?? 'google/gemini-2.5-flash' ); $sanitized['planning_model'] = sanitize_text_field( $input['planning_model'] ?? 'google/gemini-2.5-flash' ); $sanitized['writing_model'] = sanitize_text_field( $input['writing_model'] ?? 'anthropic/claude-3.5-sonnet' ); $sanitized['refinement_model'] = sanitize_text_field( $input['refinement_model'] ?? 'anthropic/claude-3.5-sonnet' ); $sanitized['image_model'] = sanitize_text_field( $input['image_model'] ?? 'openai/gpt-4o' ); // 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']; // Sanitize search options. $sanitized['search_engine'] = in_array( $input['search_engine'], array( 'auto', 'native', 'exa' ), true ) ? $input['search_engine'] : 'auto'; $sanitized['search_depth'] = isset( $input['search_depth'] ) && in_array( $input['search_depth'], array( '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'], array( '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 = array( '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'] = array( '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'] = array( 'auto', 'English', 'Indonesian' ); } // Sanitize custom languages if ( isset( $input['custom_languages'] ) && is_array( $input['custom_languages'] ) ) { $sanitized['custom_languages'] = array_filter( array_map( 'sanitize_text_field', $input['custom_languages'] ) ); } else { $sanitized['custom_languages'] = array(); } return $sanitized; } /** * Render settings page. * * @since 0.1.0 */ public function render_settings_page() { $settings = get_option( 'wp_agentic_writer_settings', array() ); // Extract settings (6 models as per model-preset-brief.md). $api_key = $settings['openrouter_api_key'] ?? ''; $chat_model = $settings['chat_model'] ?? 'google/gemini-2.5-flash'; $clarity_model = $settings['clarity_model'] ?? 'google/gemini-2.5-flash'; $planning_model = $settings['planning_model'] ?? 'google/gemini-2.5-flash'; $writing_model = $settings['writing_model'] ?? ( $settings['execution_model'] ?? 'anthropic/claude-3.5-sonnet' ); $refinement_model = $settings['refinement_model'] ?? 'anthropic/claude-3.5-sonnet'; $image_model = $settings['image_model'] ?? 'openai/gpt-4o'; $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; // Clarification quiz settings. $enable_clarification_quiz = $settings['enable_clarification_quiz'] ?? true; $clarity_confidence_threshold = $settings['clarity_confidence_threshold'] ?? '0.6'; $required_context_categories = $settings['required_context_categories'] ?? array( 'target_outcome', 'target_audience', 'tone', 'content_depth', 'expertise_level', 'content_type', 'pov', ); // 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' : '' ); ?>

🔑

OpenRouter.', 'wp-agentic-writer' ) ), 'https://openrouter.ai/keys' ); ?>

💰

$
%
$
USD
🔍

🌍

render_language_preferences( $settings ); ?>
⚙️

💰 ~$0.06/article
Chat/Clarity/Planning/Refinement: Gemini 2.5 Flash
Writing: Mistral Small Creative
Image: GPT-4o
⭐ ~$0.14/article
Chat/Clarity/Planning: Gemini 2.5 Flash
Writing/Refinement: Claude 3.5 Sonnet
Image: GPT-4o
✨ ~$0.31/article
Chat/Planning: Gemini 3 Flash Preview
Clarity: Claude Sonnet 4
Writing/Refinement: GPT-4.1
Image: GPT-4o
🤖

~$0.00 per article

render_cost_log_tab(); ?>
📖

~$0.001
~$0.0005
~$0.05-0.15
~$0.001
~$0.01-0.03
~$0.02
$0.003-0.04

Gemini 2.5 Flash $0.15 / $0.60
Gemini 3 Flash Preview $0.50 / $3.00
Claude 3.5 Sonnet $3.00 / $15.00
Mistral Small Creative $0.10 / $0.30
GPT-4.1 $2.00 / $8.00
Claude Sonnet 4 $3.00 / $15.00
GPT-4o $2.50 / $10.00
💡

: ~$0.001 : ~$0.001 : ~$0.10 : ~$0.003
: ~$0.10-0.15
: ~$0.05 : ~$0.20
: ~$1.50-2.00
: ~$0.02-0.04
'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)', ); ?>
$label ) : ?>

$lang ) : ?>

prefix . 'wpaw_cost_tracking'; // Get filter parameters $filter_post = isset( $_GET['filter_post'] ) ? intval( $_GET['filter_post'] ) : 0; $filter_model = isset( $_GET['filter_model'] ) ? sanitize_text_field( $_GET['filter_model'] ) : ''; $filter_type = isset( $_GET['filter_type'] ) ? sanitize_text_field( $_GET['filter_type'] ) : ''; $filter_date_from = isset( $_GET['filter_date_from'] ) ? sanitize_text_field( $_GET['filter_date_from'] ) : ''; $filter_date_to = isset( $_GET['filter_date_to'] ) ? sanitize_text_field( $_GET['filter_date_to'] ) : ''; $per_page = 50; $paged = isset( $_GET['paged'] ) ? max( 1, intval( $_GET['paged'] ) ) : 1; $offset = ( $paged - 1 ) * $per_page; // Build query $where = array( '1=1' ); 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 $total_items = $wpdb->get_var( "SELECT COUNT(*) FROM {$table_name} WHERE {$where_clause}" ); $total_pages = ceil( $total_items / $per_page ); // Get cost records $costs = $wpdb->get_results( "SELECT * FROM {$table_name} WHERE {$where_clause} ORDER BY created_at DESC LIMIT {$per_page} OFFSET {$offset}" ); // Get summary stats $cost_tracker = WP_Agentic_Writer_Cost_Tracker::get_instance(); $total_all_time = $wpdb->get_var( "SELECT SUM(cost) FROM {$table_name}" ); $monthly_total = $cost_tracker->get_monthly_total(); $today_total = $wpdb->get_var( $wpdb->prepare( "SELECT SUM(cost) FROM {$table_name} WHERE DATE(created_at) = %s", current_time( 'Y-m-d' ) ) ); $total_posts = $wpdb->get_var( "SELECT COUNT(DISTINCT post_id) FROM {$table_name} WHERE post_id > 0" ); $avg_per_post = $total_posts > 0 ? $total_all_time / $total_posts : 0; // Get unique models and types for filters $models = $wpdb->get_col( "SELECT DISTINCT model FROM {$table_name} ORDER BY model" ); $types = $wpdb->get_col( "SELECT DISTINCT action FROM {$table_name} ORDER BY action" ); ?>
📊

💰
$
📅
$
☀️
$
📝
$
🔍

📋

post_id ); if ( ! $post_title && $cost->post_id > 0 ) { $post_title = sprintf( __( '[Removed Post #%d]', 'wp-agentic-writer' ), $cost->post_id ); $post_link = '#'; $post_class = 'wpaw-removed-post'; } elseif ( $cost->post_id > 0 ) { $post_link = get_edit_post_link( $cost->post_id ); $post_class = ''; } else { $post_title = '-'; $post_link = '#'; $post_class = ''; } ?>
created_at ) ) ); ?> model ); ?> action ) ) ); ?> input_tokens ); ?> output_tokens ); ?> $cost, 4 ); ?>
1 ) : ?>
'wp-agentic-writer-settings', 'tab' => 'cost-log', 'filter_post' => $filter_post, 'filter_model' => $filter_model, 'filter_type' => $filter_type, 'filter_date_from' => $filter_date_from, 'filter_date_to' => $filter_date_to, ), admin_url( 'options-general.php' ) ); if ( $paged > 1 ) { printf( '« %s', esc_url( add_query_arg( 'paged', $paged - 1, $base_url ) ), esc_html__( 'Previous', 'wp-agentic-writer' ) ); } printf( '%s %d / %d', esc_html__( 'Page', 'wp-agentic-writer' ), $paged, $total_pages ); if ( $paged < $total_pages ) { printf( '%s »', esc_url( add_query_arg( 'paged', $paged + 1, $base_url ) ), esc_html__( 'Next', 'wp-agentic-writer' ) ); } ?>