From 5d5019bda362cf63eb62312548f8e59a1cd150d3 Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Tue, 14 Apr 2026 08:32:30 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20cost=20tracker=20$0.00=20bug=20=E2=80=94?= =?UTF-8?q?=20add=20token=20estimation=20fallback=20for=20DeepSeek=20(v4.4?= =?UTF-8?q?.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeepSeek models on OpenRouter omit the usage object, causing all token counts and costs to log as zero. Added character-based estimation fallback (~3.75 chars/token), token_source column to distinguish measured vs estimated data, and admin UI indicators (amber warnings, ~ prefix on estimates). Updated DeepSeek/Qwen pricing table. Co-Authored-By: Claude Opus 4.6 --- ARCHITECTURE.md | 21 +++++++++++ CHANGELOG.md | 16 ++++++++ includes/class-psa-admin.php | 26 +++++++++++-- includes/class-psa-cost-tracker.php | 58 +++++++++++++++++++++++++++-- includes/class-psa-openrouter.php | 25 ++++++++++++- peptide-search-ai.php | 4 +- 6 files changed, 138 insertions(+), 12 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ed20d80..a0fbaff 100755 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -321,6 +321,27 @@ Migration for existing peptides via admin "Migrate Existing Peptides to Categori | `psa_directory_card_extras` | Filter | Allows adding HTML to directory cards (e.g., observation count badge). | | `psa_rest_compound_data` | Filter | Allows enriching REST compound data before response. Receives `$data`, `$post_id`. | +### API Logs Table (`wp_psa_api_logs`) + +Custom table created via `dbDelta` on plugin activation. Tracks every OpenRouter API call for cost monitoring and budget enforcement. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | BIGINT AUTO_INCREMENT | Primary key | +| `created_at` | DATETIME | Timestamp of API call | +| `provider` | VARCHAR(50) | API provider (default: `openrouter`) | +| `model` | VARCHAR(100) | Model identifier (e.g., `deepseek/deepseek-v3.2`) | +| `prompt_tokens` | INT | Tokens in prompt (measured or estimated) | +| `completion_tokens` | INT | Tokens in completion (measured or estimated) | +| `total_tokens` | INT | Sum of prompt + completion tokens | +| `estimated_cost_usd` | DECIMAL(10,6) | Estimated cost in USD | +| `request_type` | VARCHAR(20) | `validation` or `generation` | +| `peptide_name` | VARCHAR(200) | Peptide being processed | +| `success` | TINYINT(1) | Whether the API call succeeded | +| `token_source` | VARCHAR(20) | `api` (measured from response), `estimated` (character-based fallback), or `none` (v4.4.3) | + +**Why `token_source`:** Some OpenRouter models (notably DeepSeek) omit the `usage` object from API responses. When this happens, `PSA_OpenRouter` estimates tokens from character length (~3.75 chars/token). The `token_source` column lets the admin UI clearly distinguish measured vs. estimated data. + ### Options (Plugin Settings) Stored in `wp_options` table: diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f2366e..55fbaa4 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to the Peptide Search AI plugin will be documented in this f The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.4.3] - 2026-04-14 + +### Fixed +- **Cost tracker $0.00 bug**: DeepSeek models on OpenRouter omit the `usage` object from API responses, causing all token counts and costs to log as zero. Added character-based token estimation fallback (~3.75 chars/token for scientific English) so spend is always tracked. + +### Added +- `token_source` column in `psa_api_logs` table distinguishing measured (`api`) vs. estimated (`estimated`) token counts. Migrated via `dbDelta` on plugin activation. +- DeepSeek and Qwen model pricing in `PSA_Cost_Tracker::estimate_cost()`: `deepseek/deepseek-v3.2`, `deepseek/deepseek-chat`, `deepseek/deepseek-r1`, `qwen/qwen3.6-plus:free`. +- `PSA_Cost_Tracker::get_monthly_estimated_count()` method for querying how many API calls used estimates in a given month. +- Admin UI indicators for estimated data: amber warning icon on monthly summary ("includes estimates"), `~` prefix on estimated token/cost values in the log table, and tooltips explaining the estimation method. +- Diagnostic logging of raw `usage` field from OpenRouter for models that report zero tokens (temporary — remove after confirming). + +### Changed +- `PSA_OpenRouter::send_request_once()` now passes `token_source` to cost tracker alongside token counts. +- Version bump to 4.4.3. + ## [4.3.0] - Unreleased ### Added diff --git a/includes/class-psa-admin.php b/includes/class-psa-admin.php index fc6a442..0e19f52 100755 --- a/includes/class-psa-admin.php +++ b/includes/class-psa-admin.php @@ -482,6 +482,7 @@ class PSA_Admin { private static function render_usage_summary(): void { $current_spend = PSA_Cost_Tracker::get_monthly_spend(); $current_tokens = PSA_Cost_Tracker::get_monthly_tokens(); + $estimated_count = PSA_Cost_Tracker::get_monthly_estimated_count(); $budget = floatval( self::get_setting( 'monthly_budget', PSA_Config::DEFAULT_MONTHLY_BUDGET ) ); $budget_remaining = ( 0.0 === $budget ) ? __( 'Unlimited', 'peptide-search-ai' ) : '$' . number_format( max( 0, $budget - $current_spend ), 2 ); $recent_logs = PSA_Cost_Tracker::get_recent_logs( 20 ); @@ -490,7 +491,11 @@ class PSA_Admin {

- $
+ $ + 0 ) : ?> + ⚠ includes estimates + +

@@ -512,11 +517,24 @@ class PSA_Admin { - + token_source ) && 'estimated' === $log->token_source ); ?> + > created_at ); ?> model ); ?> - total_tokens ) ); ?> - $estimated_cost_usd, 4 ) ); ?> + + + ~total_tokens ) ); ?> + + total_tokens ) ); ?> + + + + + ~$estimated_cost_usd, 4 ) ); ?> + + $estimated_cost_usd, 4 ) ); ?> + + request_type ); ?> peptide_name ); ?> success ) ? '✓' : '✗'; ?> diff --git a/includes/class-psa-cost-tracker.php b/includes/class-psa-cost-tracker.php index 261d88c..8853252 100755 --- a/includes/class-psa-cost-tracker.php +++ b/includes/class-psa-cost-tracker.php @@ -39,6 +39,7 @@ class PSA_Cost_Tracker { request_type VARCHAR(20) NOT NULL, peptide_name VARCHAR(200) DEFAULT '', success TINYINT(1) NOT NULL DEFAULT 1, + token_source VARCHAR(20) NOT NULL DEFAULT 'api', INDEX idx_created_at (created_at), INDEX idx_request_type (request_type) ) $charset_collate;"; @@ -66,6 +67,7 @@ class PSA_Cost_Tracker { * @type string $peptide_name Name of peptide being processed * @type bool $success Whether API call succeeded * @type float $estimated_cost_usd Cost (or 0 to auto-calculate) + * @type string $token_source 'api' (from usage response), 'estimated' (character-based), or 'none' * } * @return int|false Insert ID or false on error. */ @@ -84,13 +86,14 @@ class PSA_Cost_Tracker { $peptide_name = $data['peptide_name'] ?? ''; $success = ! empty( $data['success'] ) ? 1 : 0; $estimated_cost = floatval( $data['estimated_cost_usd'] ?? 0 ); + $token_source = $data['token_source'] ?? 'api'; // Auto-sum tokens if not provided. if ( 0 === $total_tokens && ( $prompt_tokens > 0 || $completion_tokens > 0 ) ) { $total_tokens = $prompt_tokens + $completion_tokens; } - // Auto-calculate cost if not provided. + // Auto-calculate cost if not provided and we have token data. if ( 0 === $estimated_cost && $prompt_tokens > 0 ) { $estimated_cost = self::estimate_cost( $model, $prompt_tokens, $completion_tokens ); } @@ -105,9 +108,10 @@ class PSA_Cost_Tracker { 'request_type' => sanitize_text_field( $request_type ), 'peptide_name' => sanitize_text_field( $peptide_name ), 'success' => $success, + 'token_source' => sanitize_text_field( $token_source ), ); - $insert_format = array( '%s', '%s', '%d', '%d', '%d', '%f', '%s', '%s', '%d' ); + $insert_format = array( '%s', '%s', '%d', '%d', '%d', '%f', '%s', '%s', '%d', '%s' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $result = $wpdb->insert( $table_name, $insert_data, $insert_format ); @@ -198,6 +202,45 @@ class PSA_Cost_Tracker { return (int) $result; } + /** + * Count how many API calls this month used character-based token estimates. + * + * Why: Lets the admin UI flag when reported costs include approximations + * so operators can distinguish measured spend from estimated spend. + * + * @param int|null $year Year (defaults to current year). + * @param int|null $month Month (defaults to current month). + * @return int Number of estimated rows this month. + */ + public static function get_monthly_estimated_count( ?int $year = null, ?int $month = null ): int { + global $wpdb; + + if ( null === $year ) { + $year = (int) gmdate( 'Y' ); + } + if ( null === $month ) { + $month = (int) gmdate( 'm' ); + } + + $table_name = $wpdb->prefix . 'psa_api_logs'; + $month = max( 1, min( 12, (int) $month ) ); + + $start_date = gmdate( 'Y-m-01', mktime( 0, 0, 0, $month, 1, $year ) ); + $end_date = gmdate( 'Y-m-t', mktime( 0, 0, 0, $month, 1, $year ) ) . ' 23:59:59'; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $result = $wpdb->get_var( + $wpdb->prepare( + 'SELECT COUNT(*) FROM ' . $table_name . ' WHERE created_at >= %s AND created_at <= %s AND token_source = %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- table name is safe, built from $wpdb->prefix. + $start_date, + $end_date, + 'estimated' + ) + ); + + return (int) $result; + } + /** * Check if current month's budget has been exceeded. * Budget of 0 means unlimited. @@ -251,9 +294,16 @@ class PSA_Cost_Tracker { */ public static function estimate_cost( string $model, int $prompt_tokens, int $completion_tokens ): float { // Known model pricing per 1 million tokens (input, output). + // Why: OpenRouter pricing varies by model; we maintain known rates here and + // fall back to a conservative default for unlisted models so cost tracking + // always produces a usable upper-bound estimate. $pricing = array( - 'google/gemini-2.5-flash' => array( 0.15, 0.60 ), - 'google/gemini-2.0-flash-001' => array( 0.10, 0.40 ), + 'google/gemini-2.5-flash' => array( 0.15, 0.60 ), + 'google/gemini-2.0-flash-001' => array( 0.10, 0.40 ), + 'deepseek/deepseek-v3.2' => array( 0.27, 1.10 ), + 'deepseek/deepseek-chat' => array( 0.27, 1.10 ), + 'deepseek/deepseek-r1' => array( 0.55, 2.19 ), + 'qwen/qwen3.6-plus:free' => array( 0.00, 0.00 ), ); $model = sanitize_text_field( $model ); diff --git a/includes/class-psa-openrouter.php b/includes/class-psa-openrouter.php index bd085d3..5c9c6bc 100755 --- a/includes/class-psa-openrouter.php +++ b/includes/class-psa-openrouter.php @@ -147,8 +147,28 @@ class PSA_OpenRouter { } // Log API call with token usage. - $prompt_tokens = (int) ( $body['usage']['prompt_tokens'] ?? 0 ); + $response_text = $body['choices'][0]['message']['content']; + $prompt_tokens = (int) ( $body['usage']['prompt_tokens'] ?? 0 ); $completion_tokens = (int) ( $body['usage']['completion_tokens'] ?? 0 ); + $token_source = 'api'; + + // Diagnostic: log the raw usage structure once per model so we can verify + // whether the provider returns token counts. Remove after confirming. + if ( 0 === $prompt_tokens ) { + $usage_raw = isset( $body['usage'] ) ? wp_json_encode( $body['usage'] ) : 'missing'; + error_log( 'PSA: OpenRouter usage field for model ' . $model . ': ' . substr( $usage_raw, 0, 300 ) ); + } + + // Fallback: estimate tokens from character length when the API reports zero. + // Why: Some OpenRouter models (e.g. deepseek) omit the usage object entirely. + // Approximation: ~3.75 characters per token for English scientific text. + if ( 0 === $prompt_tokens && 0 === $completion_tokens ) { + $prompt_tokens = (int) ceil( strlen( $prompt ) / 3.75 ); + $completion_tokens = (int) ceil( strlen( $response_text ) / 3.75 ); + $token_source = 'estimated'; + + error_log( 'PSA: Token usage estimated from character length for ' . $peptide_name . ': prompt=' . $prompt_tokens . ', completion=' . $completion_tokens ); + } PSA_Cost_Tracker::log_api_call( array( @@ -159,10 +179,11 @@ class PSA_OpenRouter { 'request_type' => $request_type, 'peptide_name' => $peptide_name, 'success' => true, + 'token_source' => $token_source, ) ); - return $body['choices'][0]['message']['content']; + return $response_text; } /** diff --git a/peptide-search-ai.php b/peptide-search-ai.php index d5921b4..f9f6826 100755 --- a/peptide-search-ai.php +++ b/peptide-search-ai.php @@ -3,7 +3,7 @@ * Plugin Name: Peptide Search AI * Plugin URI: https://example.com/peptide-search-ai * Description: Searchable peptide database with AI-powered auto-population and browsable directory. - * Version: 4.4.2 + * Version: 4.4.3 * Author: Terence * License: GPL v2 or later * Text Domain: peptide-search-ai @@ -15,7 +15,7 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } -define( 'PSA_VERSION', '4.4.2' ); +define( 'PSA_VERSION', '4.4.3' ); define( 'PSA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PSA_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'PSA_PLUGIN_FILE', __FILE__ );