feat: add LLM cost tracking with budget enforcement and admin dashboard

- New Cost_Tracker class: logs every API call (model, tokens, cost), enforces
  monthly budgets with hard-stop mode, provides aggregated reporting
- New llm_costs database table with indexes on created_at, model, operation
- Modified LLM class: call_openrouter_with_usage() extracts token usage from
  OpenRouter responses; budget gate checks before each API call
- Admin settings: monthly budget (USD), enforcement mode (disabled/warn/hard-stop),
  cost log retention period
- Admin cost dashboard: budget progress bar, daily cost chart (Chart.js),
  per-model and per-operation breakdowns, recent API call log
- PHPUnit tests: 16 tests covering cost calculation, pricing lookups,
  budget enforcement logic, and model validation
- Updated ARCHITECTURE.md with new table schema, data flow, and key decisions
- Updated CONVENTIONS.md with "how to add cost tracking for a new operation"
- Version bump to 2.4.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Terence
2026-04-13 04:18:30 +07:00
parent 6796121003
commit b7da4f42b9
47 changed files with 2192 additions and 164 deletions

25
includes/class-peptide-news-logger.php Normal file → Executable file
View File

@@ -1,4 +1,5 @@
<?php
declare( strict_types=1 );
/**
* Plugin Logger.
*
@@ -21,7 +22,7 @@ class Peptide_News_Logger {
/**
* Return the log table name.
*/
public static function table() {
public static function table(): string {
global $wpdb;
return $wpdb->prefix . 'peptide_news_log';
}
@@ -37,7 +38,7 @@ class Peptide_News_Logger {
* @param string $message Human-readable message.
* @param string $context Category tag (fetch, llm, admin, cron, general).
*/
public static function log( $level, $message, $context = 'general' ) {
public static function log( string $level, string $message, string $context = 'general' ): void {
global $wpdb;
$wpdb->insert(
@@ -56,22 +57,22 @@ class Peptide_News_Logger {
}
/** Convenience: info level. */
public static function info( $message, $context = 'general' ) {
public static function info( string $message, string $context = 'general' ): void {
self::log( self::LEVEL_INFO, $message, $context );
}
/** Convenience: warning level. */
public static function warning( $message, $context = 'general' ) {
public static function warning( string $message, string $context = 'general' ): void {
self::log( self::LEVEL_WARNING, $message, $context );
}
/** Convenience: error level. */
public static function error( $message, $context = 'general' ) {
public static function error( string $message, string $context = 'general' ): void {
self::log( self::LEVEL_ERROR, $message, $context );
}
/** Convenience: debug level (only writes when WP_DEBUG is on). */
public static function debug( $message, $context = 'general' ) {
public static function debug( string $message, string $context = 'general' ): void {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
self::log( self::LEVEL_DEBUG, $message, $context );
}
@@ -94,7 +95,7 @@ class Peptide_News_Logger {
* }
* @return array { 'rows' => array, 'total' => int }
*/
public static function get_logs( $args = array() ) {
public static function get_logs( array $args = array() ): array {
global $wpdb;
$defaults = array(
@@ -145,7 +146,7 @@ class Peptide_News_Logger {
/**
* Remove rows beyond the MAX_ROWS cap.
*/
private static function maybe_prune() {
private static function maybe_prune(): void {
global $wpdb;
$table = self::table();
@@ -168,7 +169,7 @@ class Peptide_News_Logger {
/**
* Clear all log entries.
*/
public static function clear() {
public static function clear(): void {
global $wpdb;
$wpdb->query( "TRUNCATE TABLE " . self::table() );
}
@@ -180,7 +181,7 @@ class Peptide_News_Logger {
/**
* AJAX: fetch paginated log entries.
*/
public static function ajax_get_logs() {
public static function ajax_get_logs(): void {
check_ajax_referer( 'peptide_news_admin', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
@@ -210,7 +211,7 @@ class Peptide_News_Logger {
/**
* AJAX: clear all log entries.
*/
public static function ajax_clear_logs() {
public static function ajax_clear_logs(): void {
check_ajax_referer( 'peptide_news_admin', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
@@ -230,7 +231,7 @@ class Peptide_News_Logger {
/**
* Create the log table (called from activator / upgrade check).
*/
public static function create_table() {
public static function create_table(): void {
global $wpdb;
$table = self::table();