maybe_upgrade_table(); } /** * Ensure table has latest schema with provider/session/status columns. * * @since 0.2.0 */ private function maybe_upgrade_table() { global $wpdb; static $checked = false; if ( $checked ) { return; } $table_name = $wpdb->prefix . 'wpaw_cost_tracking'; // Check if table exists first. $table_exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) ); if ( ! $table_exists ) { // Table missing - trigger recreation. if ( function_exists( 'wp_agentic_writer_create_cost_table' ) ) { wp_agentic_writer_create_cost_table(); } $checked = true; return; } // Check if new columns exist. $columns = $wpdb->get_col( "DESCRIBE {$table_name}", 0 ); $needs_provider = ! in_array( 'provider', $columns, true ); $needs_session = ! in_array( 'session_id', $columns, true ); $needs_status = ! in_array( 'status', $columns, true ); if ( $needs_provider || $needs_session || $needs_status ) { $alter_parts = array(); if ( $needs_provider ) { $alter_parts[] = "ADD COLUMN provider varchar(50) DEFAULT 'openrouter' AFTER action"; } if ( $needs_session ) { $alter_parts[] = "ADD COLUMN session_id varchar(32) DEFAULT '' AFTER post_id"; } if ( $needs_status ) { $alter_parts[] = "ADD COLUMN status varchar(20) DEFAULT 'success' AFTER cost"; } if ( ! empty( $alter_parts ) ) { $wpdb->query( "ALTER TABLE {$table_name} " . implode( ', ', $alter_parts ) ); } } $checked = true; } /** * Add API request to cost tracking. * * @since 0.2.0 Parameters changed: added provider, session_id, status. * @param int $post_id Post ID. * @param string $model Model name. * @param string $action Action type (planning, execution, research, image). * @param int $input_tokens Input tokens. * @param int $output_tokens Output tokens. * @param float $cost Cost in USD. * @param string $provider Provider name (optional, defaults to 'unknown'). * @param string $session_id Session ID (optional). * @param string $status Request status (optional, defaults to 'success'). */ public function add_request( $post_id, $model, $action, $input_tokens, $output_tokens, $cost, $provider = 'unknown', $session_id = '', $status = 'success' ) { global $wpdb; $table_name = $wpdb->prefix . 'wpaw_cost_tracking'; $wpdb->insert( $table_name, array( 'post_id' => $post_id, 'session_id' => $session_id, 'model' => $model, 'provider' => $provider, 'action' => $action, 'input_tokens' => $input_tokens, 'output_tokens' => $output_tokens, 'cost' => $cost, 'status' => $status, 'created_at' => current_time( 'mysql' ), ), array( '%d', '%s', '%s', '%s', '%s', '%d', '%d', '%f', '%s', '%s' ) ); } /** * Legacy add_request for backward compatibility (4 params). * * @deprecated 0.2.0 Use add_request with all parameters. * @param int $post_id Post ID. * @param string $model Model name. * @param string $action Action type. * @param int $input_tokens Input tokens. * @param int $output_tokens Output tokens. * @param float $cost Cost in USD. */ public function add_request_legacy( $post_id, $model, $action, $input_tokens, $output_tokens, $cost ) { $this->add_request( $post_id, $model, $action, $input_tokens, $output_tokens, $cost ); } /** * Record usage from WP AI Client wrapper (legacy contract). * * This method provides backward compatibility for the WP AI Client wrapper * and other callers that use a simpler interface. * * @deprecated 0.1.4 Use record_usage_full() instead for accurate provider attribution. * @since 0.1.3 * @param int $post_id Post ID. * @param string $action Action/task type (e.g., 'chat', 'planning', 'writing'). * @param string $model Model identifier. * @param float $cost Cost in USD. * @param string $session_id Session ID (optional). */ public function record_usage( $post_id, $action, $model, $cost, $session_id = '' ) { $this->record_usage_full( $post_id, $model, $action, 0, // input_tokens - not available in wrapper 0, // output_tokens - not available in wrapper $cost, 'unknown', // deprecated wrapper - provider unknown $session_id, 'success' ); } /** * Record usage with full metadata. * * Use this method when you have complete information about the request. * * @since 0.1.4 * @param int $post_id Post ID. * @param string $model Model identifier. * @param string $action Action/task type. * @param int $input_tokens Input token count. * @param int $output_tokens Output token count. * @param float $cost Cost in USD. * @param string $provider Provider name. * @param string $session_id Session ID. * @param string $status Request status. */ public function record_usage_full( $post_id, $model, $action, $input_tokens, $output_tokens, $cost, $provider, $session_id = '', $status = 'success' ) { $this->add_request( $post_id, $model, $action, $input_tokens, $output_tokens, $cost, $provider, $session_id, $status ); } /** * Get session total cost. * * @since 0.1.0 * @param int $post_id Post ID. * @return float Session total cost. */ public function get_session_total( $post_id ) { global $wpdb; $table_name = $wpdb->prefix . 'wpaw_cost_tracking'; $total = $wpdb->get_var( $wpdb->prepare( "SELECT SUM(cost) FROM {$table_name} WHERE post_id = %d", $post_id ) ); return floatval( $total ) + $this->get_image_variants_total_for_post( $post_id ); } /** * Get monthly total cost. * * @since 0.1.0 * @return float Monthly total cost. */ public function get_monthly_total() { global $wpdb; $table_name = $wpdb->prefix . 'wpaw_cost_tracking'; $month_start = date( 'Y-m-01 00:00:00' ); $total = $wpdb->get_var( $wpdb->prepare( "SELECT SUM(cost) FROM {$table_name} WHERE created_at >= %s", $month_start ) ); return floatval( $total ) + $this->get_image_variants_total_since( $month_start ); } /** * Get today's usage breakdown. * * @since 0.1.0 * @return array Today's usage. */ public function get_today_usage() { global $wpdb; $table_name = $wpdb->prefix . 'wpaw_cost_tracking'; $results = $wpdb->get_results( $wpdb->prepare( "SELECT action, SUM(input_tokens + output_tokens) as tokens, SUM(cost) as cost FROM {$table_name} WHERE created_at >= %s GROUP BY action", date( 'Y-m-d 00:00:00' ) ), ARRAY_A ); $usage = array(); $total_cost = 0; $total_tokens = 0; foreach ( $results as $row ) { $usage[ $row['action'] ] = array( 'tokens' => intval( $row['tokens'] ), 'cost' => floatval( $row['cost'] ), ); $total_cost += floatval( $row['cost'] ); $total_tokens += intval( $row['tokens'] ); } $image_cost = $this->get_image_variants_total_since( date( 'Y-m-d 00:00:00' ) ); if ( $image_cost > 0 ) { $usage['image_generation'] = array( 'tokens' => 0, 'cost' => $image_cost, ); $total_cost += $image_cost; } $usage['total'] = array( 'cost' => $total_cost, 'tokens' => $total_tokens, ); return $usage; } /** * Format cost for display. * * @since 0.1.0 * @param float $cost Cost in USD. * @return string Formatted cost. */ public function format_cost( $cost ) { return '$' . number_format( $cost, 4, '.', ',' ); } /** * Format tokens for display. * * @since 0.1.0 * @param int $tokens Number of tokens. * @return string Formatted tokens. */ public function format_tokens( $tokens ) { return number_format( $tokens ) . ' tokens'; } /** * Get detailed cost history for a post. * * @since 0.1.0 * @param int $post_id Post ID. * @param int $limit Number of records to return. * @return array Cost history records. */ public function get_post_history( $post_id, $limit = 50 ) { global $wpdb; $table_name = $wpdb->prefix . 'wpaw_cost_tracking'; $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE post_id = %d ORDER BY created_at DESC LIMIT %d", $post_id, $limit ), ARRAY_A ); $history = $results ?: array(); $image_history = $this->get_image_variants_history_for_post( $post_id, $limit ); $history = array_merge( $history, $image_history ); usort( $history, function( $a, $b ) { return strcmp( $b['created_at'] ?? '', $a['created_at'] ?? '' ); } ); return array_slice( $history, 0, $limit ); } /** * Check whether a table exists. * * @since 0.2.1 * @param string $table_name Table name. * @return bool */ private function table_exists( $table_name ) { global $wpdb; return (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ); } /** * Get image variant generation total for a post. * * @since 0.2.1 * @param int $post_id Post ID. * @return float */ private function get_image_variants_total_for_post( $post_id ) { global $wpdb; $table_name = $wpdb->prefix . 'wpaw_images_variants'; if ( $post_id <= 0 || ! $this->table_exists( $table_name ) ) { return 0.0; } $total = $wpdb->get_var( $wpdb->prepare( "SELECT SUM(cost) FROM {$table_name} WHERE post_id = %d", $post_id ) ); return floatval( $total ); } /** * Get image variant generation total since a timestamp. * * @since 0.2.1 * @param string $since MySQL datetime. * @return float */ private function get_image_variants_total_since( $since ) { global $wpdb; $table_name = $wpdb->prefix . 'wpaw_images_variants'; if ( ! $this->table_exists( $table_name ) ) { return 0.0; } $total = $wpdb->get_var( $wpdb->prepare( "SELECT SUM(cost) FROM {$table_name} WHERE created_at >= %s", $since ) ); return floatval( $total ); } /** * Get image variant generation history for a post in cost-history shape. * * @since 0.2.1 * @param int $post_id Post ID. * @param int $limit Limit. * @return array */ private function get_image_variants_history_for_post( $post_id, $limit = 50 ) { global $wpdb; $table_name = $wpdb->prefix . 'wpaw_images_variants'; if ( $post_id <= 0 || ! $this->table_exists( $table_name ) ) { return array(); } $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, post_id, image_model_used, cost, generation_time, status, created_at FROM {$table_name} WHERE post_id = %d ORDER BY created_at DESC LIMIT %d", $post_id, $limit ), ARRAY_A ); return array_map( function( $row ) { return array( 'id' => 'image_variant_' . ( $row['id'] ?? '' ), 'post_id' => (int) ( $row['post_id'] ?? 0 ), 'session_id' => '', 'model' => $row['image_model_used'] ?? '', 'provider' => 'openrouter', 'action' => 'image_generation', 'input_tokens' => 0, 'output_tokens' => 0, 'cost' => (float) ( $row['cost'] ?? 0 ), 'status' => $row['status'] ?? '', 'created_at' => $row['created_at'] ?? '', ); }, $rows ?: array() ); } /** * Get cost tracking data for frontend. * * @since 0.1.0 * @param int $post_id Post ID. * @return array Cost tracking data. */ public function get_frontend_data( $post_id = 0 ) { $today_usage = $this->get_today_usage(); $monthly_total = $this->get_monthly_total(); $session_total = $post_id > 0 ? $this->get_session_total( $post_id ) : 0; $post_history = $post_id > 0 ? $this->get_post_history( $post_id ) : array(); // Get settings for budget. $settings = get_option( 'wp_agentic_writer_settings', array() ); $monthly_budget = $settings['monthly_budget'] ?? 600; return array( 'today' => $today_usage, 'monthly' => array( 'used' => $monthly_total, 'budget' => $monthly_budget, 'percentage' => $monthly_budget > 0 ? ( $monthly_total / $monthly_budget ) * 100 : 0, 'remaining' => max( 0, $monthly_budget - $monthly_total ), ), 'session' => $session_total, 'history' => $post_history, ); } }