wpmind/modules/cost-control/includes/UsageTracker.php
LinuxJoy 74db7a20af refactor: 方法名 camelCase → snake_case (39文件, WordPress 编码规范)
将项目自有代码的方法名从 camelCase 统一为 snake_case,
符合 WordPress PHP 编码规范。涉及 Routing、Failover、Budget、
Analytics、Usage、ErrorHandler 等模块。外部库接口方法和
Image Provider 子系统保持不变。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 03:21:31 +08:00

539 lines
16 KiB
PHP

<?php
/**
* Usage Tracker - Token 用量追踪
*
* 记录每个 Provider 的 token 用量和成本估算
*
* @package WPMind\Modules\CostControl
* @since 1.0.0
*/
declare(strict_types=1);
namespace WPMind\Modules\CostControl;
use WPMind\Usage\Pricing;
class UsageTracker
{
private const OPTION_KEY = 'wpmind_usage_stats';
private const HISTORY_KEY = 'wpmind_usage_history';
private const MAX_HISTORY = 1000; // 最多保留 1000 条记录
/**
* 各服务商的定价 (per 1M tokens)
*
* @deprecated Use WPMind\Usage\Pricing::DATA instead.
*/
private const PRICING = [];
/**
* 记录一次 API 调用的 token 用量
*
* @param string $provider Provider ID
* @param string $model 模型名称
* @param int $inputTokens 输入 tokens
* @param int $outputTokens 输出 tokens
* @param int $latencyMs 延迟(毫秒)
*/
public static function record(
string $provider,
string $model,
int $inputTokens,
int $outputTokens,
int $latencyMs = 0
): void {
// 输入验证:确保 tokens 非负
$inputTokens = max(0, $inputTokens);
$outputTokens = max(0, $outputTokens);
$latencyMs = max(0, $latencyMs);
// 计算成本
$cost = self::calculate_cost($provider, $model, $inputTokens, $outputTokens);
// 更新汇总统计
self::update_stats($provider, $model, $inputTokens, $outputTokens, $cost);
// 只有有 token 使用时才添加到历史记录
if ($inputTokens > 0 || $outputTokens > 0) {
self::add_to_history($provider, $model, $inputTokens, $outputTokens, $cost, $latencyMs);
}
}
/**
* 计算成本
*
* @return float 成本(按服务商货币计价)
*/
public static function calculate_cost(
string $provider,
string $model,
int $inputTokens,
int $outputTokens
): float {
$allPricing = self::get_pricing();
$pricing = $allPricing[$provider] ?? [];
$modelPricing = $pricing[$model] ?? $pricing['default'] ?? ['input' => 0, 'output' => 0];
$inputCost = ($inputTokens / 1_000_000) * ($modelPricing['input'] ?? 0);
$outputCost = ($outputTokens / 1_000_000) * ($modelPricing['output'] ?? 0);
return round($inputCost + $outputCost, 6);
}
/**
* 获取服务商的货币类型
*
* @param string $provider Provider ID
* @return string USD 或 CNY
*/
public static function get_currency(string $provider): string
{
$allPricing = self::get_pricing();
return $allPricing[$provider]['currency'] ?? 'USD';
}
/**
* 更新汇总统计(带并发锁)
*/
private static function update_stats(
string $provider,
string $model,
int $inputTokens,
int $outputTokens,
float $cost
): void {
$lock_key = 'wpmind_usage_stats_lock';
$max_retries = 3;
$retry_delay = 50000; // 50ms
// 尝试获取锁
for ($i = 0; $i < $max_retries; $i++) {
if (false === get_transient($lock_key)) {
set_transient($lock_key, true, 5);
break;
}
usleep($retry_delay);
}
try {
$stats = get_option(self::OPTION_KEY, []);
if (!is_array($stats)) {
$stats = [];
}
$today = wp_date('Y-m-d');
$month = wp_date('Y-m');
$currency = self::get_currency($provider);
// 初始化结构
if (!isset($stats['providers']) || !is_array($stats['providers'])) {
$stats['providers'] = [];
}
if (!isset($stats['providers'][$provider])) {
$stats['providers'][$provider] = [
'total_input_tokens' => 0,
'total_output_tokens' => 0,
'total_cost' => 0,
'request_count' => 0,
'models' => [],
];
}
if (!isset($stats['daily']) || !is_array($stats['daily'])) {
$stats['daily'] = [];
}
if (!isset($stats['daily'][$today])) {
$stats['daily'][$today] = [
'input_tokens' => 0,
'output_tokens' => 0,
'cost_usd' => 0,
'cost_cny' => 0,
'requests' => 0,
];
}
if (!isset($stats['monthly']) || !is_array($stats['monthly'])) {
$stats['monthly'] = [];
}
if (!isset($stats['monthly'][$month])) {
$stats['monthly'][$month] = [
'input_tokens' => 0,
'output_tokens' => 0,
'cost_usd' => 0,
'cost_cny' => 0,
'requests' => 0,
];
}
// 更新 Provider 统计
$stats['providers'][$provider]['total_input_tokens'] += $inputTokens;
$stats['providers'][$provider]['total_output_tokens'] += $outputTokens;
$stats['providers'][$provider]['total_cost'] += $cost;
$stats['providers'][$provider]['request_count']++;
// 更新模型统计
if (!isset($stats['providers'][$provider]['models'][$model])) {
$stats['providers'][$provider]['models'][$model] = [
'input_tokens' => 0,
'output_tokens' => 0,
'cost' => 0,
'requests' => 0,
];
}
$stats['providers'][$provider]['models'][$model]['input_tokens'] += $inputTokens;
$stats['providers'][$provider]['models'][$model]['output_tokens'] += $outputTokens;
$stats['providers'][$provider]['models'][$model]['cost'] += $cost;
$stats['providers'][$provider]['models'][$model]['requests']++;
// 更新日统计(分货币)
$stats['daily'][$today]['input_tokens'] += $inputTokens;
$stats['daily'][$today]['output_tokens'] += $outputTokens;
if ($currency === 'CNY') {
$stats['daily'][$today]['cost_cny'] = ($stats['daily'][$today]['cost_cny'] ?? 0) + $cost;
} else {
$stats['daily'][$today]['cost_usd'] = ($stats['daily'][$today]['cost_usd'] ?? 0) + $cost;
}
$stats['daily'][$today]['requests']++;
// 更新月统计(分货币)
$stats['monthly'][$month]['input_tokens'] += $inputTokens;
$stats['monthly'][$month]['output_tokens'] += $outputTokens;
if ($currency === 'CNY') {
$stats['monthly'][$month]['cost_cny'] = ($stats['monthly'][$month]['cost_cny'] ?? 0) + $cost;
} else {
$stats['monthly'][$month]['cost_usd'] = ($stats['monthly'][$month]['cost_usd'] ?? 0) + $cost;
}
$stats['monthly'][$month]['requests']++;
// 更新总计(分货币)
if (!isset($stats['total']) || !is_array($stats['total'])) {
$stats['total'] = [
'input_tokens' => 0,
'output_tokens' => 0,
'cost_usd' => 0,
'cost_cny' => 0,
'requests' => 0,
];
}
$stats['total']['input_tokens'] = ($stats['total']['input_tokens'] ?? 0) + $inputTokens;
$stats['total']['output_tokens'] = ($stats['total']['output_tokens'] ?? 0) + $outputTokens;
if ($currency === 'CNY') {
$stats['total']['cost_cny'] = ($stats['total']['cost_cny'] ?? 0) + $cost;
} else {
$stats['total']['cost_usd'] = ($stats['total']['cost_usd'] ?? 0) + $cost;
}
$stats['total']['requests'] = ($stats['total']['requests'] ?? 0) + 1;
$stats['last_updated'] = time();
// 清理旧的日统计(保留 30 天)
$cutoffDate = wp_date('Y-m-d', strtotime('-30 days'));
foreach (array_keys($stats['daily'] ?? []) as $date) {
if ($date < $cutoffDate) {
unset($stats['daily'][$date]);
}
}
// 清理旧的月统计(保留 12 个月)
$cutoffMonth = wp_date('Y-m', strtotime('-12 months'));
foreach (array_keys($stats['monthly'] ?? []) as $m) {
if ($m < $cutoffMonth) {
unset($stats['monthly'][$m]);
}
}
update_option(self::OPTION_KEY, $stats, false);
wp_cache_set('wpmind_usage_stats', $stats, '', 300);
} finally {
delete_transient($lock_key);
}
}
/**
* 添加到历史记录(带并发锁)
*/
private static function add_to_history(
string $provider,
string $model,
int $inputTokens,
int $outputTokens,
float $cost,
int $latencyMs
): void {
$lock_key = 'wpmind_usage_history_lock';
$max_retries = 3;
$retry_delay = 50000;
for ($i = 0; $i < $max_retries; $i++) {
if (false === get_transient($lock_key)) {
set_transient($lock_key, true, 5);
break;
}
usleep($retry_delay);
}
try {
$history = get_option(self::HISTORY_KEY, []);
if (!is_array($history)) {
$history = [];
}
$history[] = [
'provider' => $provider,
'model' => $model,
'input_tokens' => $inputTokens,
'output_tokens' => $outputTokens,
'cost' => $cost,
'latency_ms' => $latencyMs,
'timestamp' => time(),
];
if (count($history) > self::MAX_HISTORY) {
$history = array_slice($history, -self::MAX_HISTORY);
}
update_option(self::HISTORY_KEY, $history, false);
wp_cache_set('wpmind_usage_history', $history, '', 300);
} finally {
delete_transient($lock_key);
}
}
/**
* 获取汇总统计
*/
public static function get_stats(): array
{
$cache_key = 'wpmind_usage_stats';
$stats = wp_cache_get($cache_key);
if (false === $stats) {
$stats = get_option(self::OPTION_KEY, []);
wp_cache_set($cache_key, $stats, '', 300);
}
if (!is_array($stats)) {
$stats = [];
}
return $stats ?: [
'providers' => [],
'daily' => [],
'monthly' => [],
'total' => [
'input_tokens' => 0,
'output_tokens' => 0,
'cost_usd' => 0,
'cost_cny' => 0,
'requests' => 0,
],
];
}
/**
* 获取今日统计
*/
public static function get_today_stats(): array
{
$stats = self::get_stats();
$today = wp_date('Y-m-d');
return $stats['daily'][$today] ?? [
'input_tokens' => 0,
'output_tokens' => 0,
'cost_usd' => 0,
'cost_cny' => 0,
'requests' => 0,
];
}
/**
* 获取本月统计
*/
public static function get_month_stats(): array
{
$stats = self::get_stats();
$month = wp_date('Y-m');
return $stats['monthly'][$month] ?? [
'input_tokens' => 0,
'output_tokens' => 0,
'cost_usd' => 0,
'cost_cny' => 0,
'requests' => 0,
];
}
/**
* 获取本周统计(周一到周日)
*/
public static function get_week_stats(): array
{
$stats = self::get_stats();
$daily = $stats['daily'] ?? [];
// 使用 WordPress 时区计算本周的日期范围
$weekStart = wp_date('Y-m-d', strtotime('monday this week'));
$today = wp_date('Y-m-d');
$result = [
'input_tokens' => 0,
'output_tokens' => 0,
'cost_usd' => 0,
'cost_cny' => 0,
'requests' => 0,
];
// 汇总本周每天的数据
foreach ($daily as $date => $dayStats) {
if ($date >= $weekStart && $date <= $today) {
$result['input_tokens'] += $dayStats['input_tokens'] ?? 0;
$result['output_tokens'] += $dayStats['output_tokens'] ?? 0;
$result['cost_usd'] += $dayStats['cost_usd'] ?? 0;
$result['cost_cny'] += $dayStats['cost_cny'] ?? 0;
$result['requests'] += $dayStats['requests'] ?? 0;
}
}
return $result;
}
/**
* 获取历史记录
*/
public static function get_history(int $limit = 50): array
{
$cache_key = 'wpmind_usage_history';
$history = wp_cache_get($cache_key);
if (false === $history) {
$history = get_option(self::HISTORY_KEY, []);
wp_cache_set($cache_key, $history, '', 300);
}
if (!is_array($history)) {
$history = [];
}
return array_slice(array_reverse($history), 0, $limit);
}
/**
* 获取定价信息
*/
public static function get_pricing(): array
{
if (!class_exists(Pricing::class)) {
require_once WPMIND_PATH . 'includes/Usage/Pricing.php';
}
return Pricing::DATA;
}
/**
* 清除所有统计数据
*/
public static function clear_all(): void
{
delete_option(self::OPTION_KEY);
delete_option(self::HISTORY_KEY);
wp_cache_delete('wpmind_usage_stats');
wp_cache_delete('wpmind_usage_history');
}
/**
* 格式化 token 数量
*/
public static function format_tokens(int $tokens): string
{
if ($tokens >= 1_000_000) {
return round($tokens / 1_000_000, 2) . 'M';
}
if ($tokens >= 1_000) {
return round($tokens / 1_000, 1) . 'K';
}
return (string) $tokens;
}
/**
* 格式化成本
*/
public static function format_cost(float $cost, string $currency = 'USD'): string
{
$symbol = $currency === 'CNY' ? '¥' : '$';
if ($cost < 0.01) {
return $symbol . number_format($cost, 4);
}
return $symbol . number_format($cost, 2);
}
/**
* 格式化分货币费用显示
*/
public static function format_cost_by_currency(float $costUsd, float $costCny): string
{
$parts = [];
if ($costUsd > 0) {
$parts[] = self::format_cost($costUsd, 'USD');
}
if ($costCny > 0) {
$parts[] = self::format_cost($costCny, 'CNY');
}
if (empty($parts)) {
return '$0.00';
}
return implode(' / ', $parts);
}
/**
* 获取 Provider 的显示名称
*/
public static function get_provider_display_name(string $provider): string
{
$names = [
'openai' => 'OpenAI',
'anthropic' => 'Anthropic',
'google' => 'Google AI',
'deepseek' => 'DeepSeek',
'qwen' => '通义千问',
'zhipu' => '智谱 AI',
'moonshot' => 'Moonshot',
'doubao' => '豆包',
'siliconflow' => '硅基流动',
'baidu' => '百度文心',
'minimax' => 'MiniMax',
];
return $names[$provider] ?? $provider;
}
/**
* 获取 Provider 的 Remixicon 图标类
*/
public static function get_provider_icon(string $provider): string
{
$icons = [
'openai' => 'ri-openai-line',
'anthropic' => 'ri-claude-line',
'google' => 'ri-gemini-line',
'deepseek' => 'ri-deepseek-line',
'qwen' => 'ri-qwen-ai-line',
'zhipu' => 'ri-zhipu-ai-line',
'moonshot' => 'ri-moon-line',
'doubao' => 'ri-fire-line',
'siliconflow' => 'ri-cpu-line',
'baidu' => 'ri-baidu-line',
'minimax' => 'ri-sparkling-line',
];
return $icons[$provider] ?? 'ri-robot-line';
}
/**
* 获取 Provider 的图标颜色
*/
public static function get_provider_color(string $provider): string
{
// 统一使用单色,与 WordPress 后台风格一致
return '#50575e';
}
}