feat: add cost/token tracking, monthly budget system, and dry-run mode

- Create PSA_Cost_Tracker class with database table for API call logging
- Log prompt/completion tokens and estimated cost for every OpenRouter API call
- Implement monthly budget system with hard stop (budget of 0 = unlimited)
- Add budget checks in validate_peptide_name() and generate_peptide_content()
- Add dry_run() method to estimate costs before making API calls
- Display monthly usage summary and recent API logs in admin settings
- Add monthly_budget setting to admin interface with number input field
- Update uninstall.php to drop psa_api_logs table on plugin removal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
peptiderepo
2026-04-11 18:59:19 +07:00
parent 4770f0a861
commit 4a5f2711a9
18 changed files with 2642 additions and 13 deletions

View File

@@ -92,6 +92,13 @@ class PSA_Admin {
'peptide-search-ai',
'psa_behavior_section'
);
add_settings_field(
'monthly_budget',
__( 'Monthly API Budget (USD)', 'peptide-search-ai' ),
array( __CLASS__, 'render_budget_field' ),
'peptide-search-ai',
'psa_behavior_section'
);
}
/**
@@ -123,6 +130,7 @@ class PSA_Admin {
'auto_publish' => sanitize_text_field( $input['auto_publish'] ?? 'draft' ),
// Checkbox: if key is absent in POST, it was unchecked → '0'.
'use_pubchem' => ! empty( $input['use_pubchem'] ) ? '1' : '0',
'monthly_budget' => max( 0, floatval( $input['monthly_budget'] ?? PSA_Config::DEFAULT_MONTHLY_BUDGET ) ),
);
}
@@ -210,6 +218,14 @@ class PSA_Admin {
<?php
}
public static function render_budget_field() {
$value = self::get_setting( 'monthly_budget', PSA_Config::DEFAULT_MONTHLY_BUDGET );
?>
<input type="number" name="psa_settings[monthly_budget]" value="<?php echo esc_attr( $value ); ?>" step="0.01" min="0" />
<p class="description"><?php esc_html_e( 'Set to 0 for unlimited. Generation will stop when this budget is reached.', 'peptide-search-ai' ); ?></p>
<?php
}
// -------------------------------------------------------------------------
// Settings page
// -------------------------------------------------------------------------
@@ -227,6 +243,10 @@ class PSA_Admin {
?>
</form>
<hr />
<h2><?php esc_html_e( 'API Usage & Costs', 'peptide-search-ai' ); ?></h2>
<?php self::render_usage_summary(); ?>
<hr />
<h2><?php esc_html_e( 'How It Works', 'peptide-search-ai' ); ?></h2>
<ol style="max-width:600px; line-height:1.8;">
@@ -250,6 +270,58 @@ class PSA_Admin {
<?php
}
/**
* Render the API usage summary section.
*/
private static function render_usage_summary() {
$current_spend = PSA_Cost_Tracker::get_monthly_spend();
$current_tokens = PSA_Cost_Tracker::get_monthly_tokens();
$budget = floatval( self::get_setting( 'monthly_budget', PSA_Config::DEFAULT_MONTHLY_BUDGET ) );
$budget_remaining = ( 0 === $budget ) ? __( 'Unlimited', 'peptide-search-ai' ) : '$' . number_format( max( 0, $budget - $current_spend ), 2 );
$recent_logs = PSA_Cost_Tracker::get_recent_logs( 20 );
?>
<div style="background: #f5f5f5; padding: 15px; border-radius: 4px; margin-bottom: 20px;">
<h3><?php esc_html_e( 'This Month', 'peptide-search-ai' ); ?></h3>
<p>
<strong><?php esc_html_e( 'Spend:', 'peptide-search-ai' ); ?></strong> $<?php echo esc_html( number_format( $current_spend, 2 ) ); ?><br />
<strong><?php esc_html_e( 'Tokens:', 'peptide-search-ai' ); ?></strong> <?php echo esc_html( number_format( $current_tokens ) ); ?><br />
<strong><?php esc_html_e( 'Budget Remaining:', 'peptide-search-ai' ); ?></strong> <?php echo esc_html( $budget_remaining ); ?>
</p>
</div>
<?php if ( ! empty( $recent_logs ) ) : ?>
<h3><?php esc_html_e( 'Recent API Calls (Last 20)', 'peptide-search-ai' ); ?></h3>
<table class="widefat striped" style="margin-bottom: 20px;">
<thead>
<tr>
<th><?php esc_html_e( 'Date', 'peptide-search-ai' ); ?></th>
<th><?php esc_html_e( 'Model', 'peptide-search-ai' ); ?></th>
<th><?php esc_html_e( 'Tokens', 'peptide-search-ai' ); ?></th>
<th><?php esc_html_e( 'Cost', 'peptide-search-ai' ); ?></th>
<th><?php esc_html_e( 'Type', 'peptide-search-ai' ); ?></th>
<th><?php esc_html_e( 'Peptide', 'peptide-search-ai' ); ?></th>
<th><?php esc_html_e( 'Status', 'peptide-search-ai' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $recent_logs as $log ) : ?>
<tr>
<td><?php echo esc_html( $log->created_at ); ?></td>
<td><code><?php echo esc_html( $log->model ); ?></code></td>
<td><?php echo esc_html( number_format( $log->total_tokens ) ); ?></td>
<td>$<?php echo esc_html( number_format( $log->estimated_cost_usd, 4 ) ); ?></td>
<td><?php echo esc_html( $log->request_type ); ?></td>
<td><?php echo esc_html( $log->peptide_name ); ?></td>
<td><?php echo ( 1 === (int) $log->success ) ? '✓' : '✗'; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php
}
/**
* Helper to get a single setting with a default.
*