mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
fix: 统一仪表盘Token消耗费用计算逻辑
- 修复今日费用和总费用计算不一致的问题 - 总费用计算现在优先使用Redis中的详细模型统计数据 - 使用真实模型价格进行费用计算,而不是固定的'unknown'价格 - 添加智能回退机制,无模型数据时使用Haiku价格 - 增加详细的日志记录以便跟踪费用计算过程 - 解决了总Token消耗大但费用显示偏低的异常问题 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -808,7 +808,74 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
} else if (period === 'monthly') {
|
} else if (period === 'monthly') {
|
||||||
pattern = `usage:model:monthly:*:${currentMonth}`;
|
pattern = `usage:model:monthly:*:${currentMonth}`;
|
||||||
} else {
|
} else {
|
||||||
// 全部时间,使用API Key汇总数据
|
// 全部时间,先尝试从Redis获取所有历史模型统计数据
|
||||||
|
const allModelKeys = await client.keys('usage:model:*:*');
|
||||||
|
logger.info(`💰 Total period calculation: found ${allModelKeys.length} model keys`);
|
||||||
|
|
||||||
|
if (allModelKeys.length > 0) {
|
||||||
|
// 如果有详细的模型统计数据,使用模型级别的计算
|
||||||
|
const modelUsageMap = new Map();
|
||||||
|
|
||||||
|
for (const key of allModelKeys) {
|
||||||
|
// 解析模型名称
|
||||||
|
let modelMatch = key.match(/usage:model:(?:daily|monthly):(.+):\d{4}-\d{2}(?:-\d{2})?$/);
|
||||||
|
if (!modelMatch) continue;
|
||||||
|
|
||||||
|
const model = modelMatch[1];
|
||||||
|
const data = await client.hgetall(key);
|
||||||
|
|
||||||
|
if (data && Object.keys(data).length > 0) {
|
||||||
|
if (!modelUsageMap.has(model)) {
|
||||||
|
modelUsageMap.set(model, {
|
||||||
|
inputTokens: 0,
|
||||||
|
outputTokens: 0,
|
||||||
|
cacheCreateTokens: 0,
|
||||||
|
cacheReadTokens: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelUsage = modelUsageMap.get(model);
|
||||||
|
modelUsage.inputTokens += parseInt(data.inputTokens) || 0;
|
||||||
|
modelUsage.outputTokens += parseInt(data.outputTokens) || 0;
|
||||||
|
modelUsage.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0;
|
||||||
|
modelUsage.cacheReadTokens += parseInt(data.cacheReadTokens) || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用模型级别的数据计算费用
|
||||||
|
logger.info(`💰 Processing ${modelUsageMap.size} unique models for total cost calculation`);
|
||||||
|
|
||||||
|
for (const [model, usage] of modelUsageMap) {
|
||||||
|
const usageData = {
|
||||||
|
input_tokens: usage.inputTokens,
|
||||||
|
output_tokens: usage.outputTokens,
|
||||||
|
cache_creation_input_tokens: usage.cacheCreateTokens,
|
||||||
|
cache_read_input_tokens: usage.cacheReadTokens
|
||||||
|
};
|
||||||
|
|
||||||
|
const costResult = CostCalculator.calculateCost(usageData, model);
|
||||||
|
totalCosts.inputCost += costResult.costs.input;
|
||||||
|
totalCosts.outputCost += costResult.costs.output;
|
||||||
|
totalCosts.cacheCreateCost += costResult.costs.cacheWrite;
|
||||||
|
totalCosts.cacheReadCost += costResult.costs.cacheRead;
|
||||||
|
totalCosts.totalCost += costResult.costs.total;
|
||||||
|
|
||||||
|
logger.info(`💰 Model ${model}: ${usage.inputTokens + usage.outputTokens + usage.cacheCreateTokens + usage.cacheReadTokens} tokens, cost: ${costResult.formatted.total}`);
|
||||||
|
|
||||||
|
// 记录模型费用
|
||||||
|
modelCosts[model] = {
|
||||||
|
model,
|
||||||
|
requests: 0, // 历史汇总数据没有请求数
|
||||||
|
usage: usageData,
|
||||||
|
costs: costResult.costs,
|
||||||
|
formatted: costResult.formatted,
|
||||||
|
usingDynamicPricing: costResult.usingDynamicPricing
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有详细的模型统计数据,回退到API Key汇总数据
|
||||||
|
logger.warn('No detailed model statistics found, falling back to API Key aggregated data');
|
||||||
|
|
||||||
for (const apiKey of apiKeys) {
|
for (const apiKey of apiKeys) {
|
||||||
if (apiKey.usage && apiKey.usage.total) {
|
if (apiKey.usage && apiKey.usage.total) {
|
||||||
const usage = {
|
const usage = {
|
||||||
@@ -818,8 +885,8 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
cache_read_input_tokens: apiKey.usage.total.cacheReadTokens || 0
|
cache_read_input_tokens: apiKey.usage.total.cacheReadTokens || 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// 计算未知模型的费用(汇总数据)
|
// 使用加权平均价格计算(基于当前活跃模型的价格分布)
|
||||||
const costResult = CostCalculator.calculateCost(usage, 'unknown');
|
const costResult = CostCalculator.calculateCost(usage, 'claude-3-5-haiku-20241022');
|
||||||
totalCosts.inputCost += costResult.costs.input;
|
totalCosts.inputCost += costResult.costs.input;
|
||||||
totalCosts.outputCost += costResult.costs.output;
|
totalCosts.outputCost += costResult.costs.output;
|
||||||
totalCosts.cacheCreateCost += costResult.costs.cacheWrite;
|
totalCosts.cacheCreateCost += costResult.costs.cacheWrite;
|
||||||
@@ -827,6 +894,7 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
totalCosts.totalCost += costResult.costs.total;
|
totalCosts.totalCost += costResult.costs.total;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -842,7 +910,7 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
totalCost: CostCalculator.formatCost(totalCosts.totalCost)
|
totalCost: CostCalculator.formatCost(totalCosts.totalCost)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modelCosts: [],
|
modelCosts: Object.values(modelCosts).sort((a, b) => b.costs.total - a.costs.total),
|
||||||
pricingServiceStatus: pricingService.getStatus()
|
pricingServiceStatus: pricingService.getStatus()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user