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:
82
tests/unit/LlmUsageTest.php
Executable file
82
tests/unit/LlmUsageTest.php
Executable file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
/**
|
||||
* Unit tests for LLM cost tracking integration.
|
||||
*
|
||||
* Tests that call_openrouter_with_usage() correctly structures its
|
||||
* return array and that the backward-compatible call_openrouter()
|
||||
* wrapper still works.
|
||||
*
|
||||
* @since 2.4.0
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class LlmUsageTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
global $_test_options;
|
||||
$_test_options = array();
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// is_valid_model() — unchanged, regression check
|
||||
// =============================================
|
||||
|
||||
public function test_valid_model_format(): void {
|
||||
$this->assertTrue( Peptide_News_LLM::is_valid_model( 'google/gemini-2.0-flash-001' ) );
|
||||
$this->assertTrue( Peptide_News_LLM::is_valid_model( 'anthropic/claude-3.5-sonnet' ) );
|
||||
$this->assertTrue( Peptide_News_LLM::is_valid_model( 'google/gemma-3-27b-it:free' ) );
|
||||
}
|
||||
|
||||
public function test_invalid_model_format(): void {
|
||||
$this->assertFalse( Peptide_News_LLM::is_valid_model( '' ) );
|
||||
$this->assertFalse( Peptide_News_LLM::is_valid_model( 'no-slash-model' ) );
|
||||
$this->assertFalse( Peptide_News_LLM::is_valid_model( 'has spaces/model' ) );
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// is_enabled() — budget-aware checks
|
||||
// =============================================
|
||||
|
||||
public function test_is_enabled_false_when_no_api_key(): void {
|
||||
global $_test_options;
|
||||
$_test_options['peptide_news_llm_enabled'] = 1;
|
||||
$_test_options['peptide_news_openrouter_api_key'] = '';
|
||||
|
||||
$this->assertFalse( Peptide_News_LLM::is_enabled() );
|
||||
}
|
||||
|
||||
public function test_is_enabled_false_when_disabled(): void {
|
||||
global $_test_options;
|
||||
$_test_options['peptide_news_llm_enabled'] = 0;
|
||||
$_test_options['peptide_news_openrouter_api_key'] = 'sk-test-key';
|
||||
|
||||
$this->assertFalse( Peptide_News_LLM::is_enabled() );
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// process_article() — budget gate behavior
|
||||
// =============================================
|
||||
|
||||
public function test_process_article_returns_early_when_disabled(): void {
|
||||
global $_test_options;
|
||||
$_test_options['peptide_news_llm_enabled'] = 0;
|
||||
$_test_options['peptide_news_openrouter_api_key'] = '';
|
||||
|
||||
$article = (object) array(
|
||||
'id' => 1,
|
||||
'title' => 'Test Article',
|
||||
'excerpt' => 'Test excerpt.',
|
||||
'content' => 'Test content.',
|
||||
'categories' => 'test',
|
||||
'tags' => '',
|
||||
'ai_summary' => '',
|
||||
);
|
||||
|
||||
$result = Peptide_News_LLM::process_article( $article );
|
||||
|
||||
$this->assertFalse( $result['success'] );
|
||||
$this->assertEmpty( $result['keywords'] );
|
||||
$this->assertEmpty( $result['summary'] );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user