mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: 修复API Keys使用趋势图费用计算不准确的问题
- 使用模型级别的详细统计数据计算费用,而非固定模型 - 按实际使用的模型(sonnet/opus/haiku等)分别计算价格 - 累加各模型费用得到准确的总费用 - 降级方案改用sonnet模型(中等价格)而非haiku(最低价格) 问题原因:之前使用固定的haiku模型计算所有token的费用,导致价格偏低 解决方案:获取模型级别的使用数据,按实际模型价格计算 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1978,6 +1978,8 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
apiKeys: {}
|
apiKeys: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 先收集基础数据
|
||||||
|
const apiKeyDataMap = new Map();
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const match = key.match(/usage:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/);
|
const match = key.match(/usage:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/);
|
||||||
if (!match) continue;
|
if (!match) continue;
|
||||||
@@ -1992,25 +1994,74 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0;
|
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0;
|
||||||
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
||||||
|
|
||||||
// 计算费用 - 使用默认模型价格,因为小时级别的数据没有模型信息
|
apiKeyDataMap.set(apiKeyId, {
|
||||||
const usage = {
|
|
||||||
input_tokens: inputTokens,
|
|
||||||
output_tokens: outputTokens,
|
|
||||||
cache_creation_input_tokens: cacheCreateTokens,
|
|
||||||
cache_read_input_tokens: cacheReadTokens
|
|
||||||
};
|
|
||||||
const costResult = CostCalculator.calculateCost(usage, 'claude-3-5-haiku-20241022');
|
|
||||||
|
|
||||||
hourData.apiKeys[apiKeyId] = {
|
|
||||||
name: apiKeyMap.get(apiKeyId).name,
|
name: apiKeyMap.get(apiKeyId).name,
|
||||||
tokens: totalTokens,
|
tokens: totalTokens,
|
||||||
requests: parseInt(data.requests) || 0,
|
requests: parseInt(data.requests) || 0,
|
||||||
cost: costResult.costs.total,
|
inputTokens,
|
||||||
formattedCost: costResult.formatted.total
|
outputTokens,
|
||||||
};
|
cacheCreateTokens,
|
||||||
|
cacheReadTokens
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取该小时的模型级别数据来计算准确费用
|
||||||
|
const modelPattern = `usage:*:model:hourly:*:${hourKey}`;
|
||||||
|
const modelKeys = await client.keys(modelPattern);
|
||||||
|
const apiKeyCostMap = new Map();
|
||||||
|
|
||||||
|
for (const modelKey of modelKeys) {
|
||||||
|
const match = modelKey.match(/usage:(.+?):model:hourly:(.+?):\d{4}-\d{2}-\d{2}:\d{2}/);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const apiKeyId = match[1];
|
||||||
|
const model = match[2];
|
||||||
|
const modelData = await client.hgetall(modelKey);
|
||||||
|
|
||||||
|
if (modelData && apiKeyDataMap.has(apiKeyId)) {
|
||||||
|
const usage = {
|
||||||
|
input_tokens: parseInt(modelData.inputTokens) || 0,
|
||||||
|
output_tokens: parseInt(modelData.outputTokens) || 0,
|
||||||
|
cache_creation_input_tokens: parseInt(modelData.cacheCreateTokens) || 0,
|
||||||
|
cache_read_input_tokens: parseInt(modelData.cacheReadTokens) || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const costResult = CostCalculator.calculateCost(usage, model);
|
||||||
|
const currentCost = apiKeyCostMap.get(apiKeyId) || 0;
|
||||||
|
apiKeyCostMap.set(apiKeyId, currentCost + costResult.costs.total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组合数据
|
||||||
|
for (const [apiKeyId, data] of apiKeyDataMap) {
|
||||||
|
const cost = apiKeyCostMap.get(apiKeyId) || 0;
|
||||||
|
|
||||||
|
// 如果没有模型级别数据,使用默认模型计算(降级方案)
|
||||||
|
let finalCost = cost;
|
||||||
|
let formattedCost = CostCalculator.formatCost(cost);
|
||||||
|
|
||||||
|
if (cost === 0 && data.tokens > 0) {
|
||||||
|
const usage = {
|
||||||
|
input_tokens: data.inputTokens,
|
||||||
|
output_tokens: data.outputTokens,
|
||||||
|
cache_creation_input_tokens: data.cacheCreateTokens,
|
||||||
|
cache_read_input_tokens: data.cacheReadTokens
|
||||||
|
};
|
||||||
|
const fallbackResult = CostCalculator.calculateCost(usage, 'claude-3-5-sonnet-20241022');
|
||||||
|
finalCost = fallbackResult.costs.total;
|
||||||
|
formattedCost = fallbackResult.formatted.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
hourData.apiKeys[apiKeyId] = {
|
||||||
|
name: data.name,
|
||||||
|
tokens: data.tokens,
|
||||||
|
requests: data.requests,
|
||||||
|
cost: finalCost,
|
||||||
|
formattedCost: formattedCost
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
trendData.push(hourData);
|
trendData.push(hourData);
|
||||||
currentHour.setHours(currentHour.getHours() + 1);
|
currentHour.setHours(currentHour.getHours() + 1);
|
||||||
}
|
}
|
||||||
@@ -2035,6 +2086,8 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
apiKeys: {}
|
apiKeys: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 先收集基础数据
|
||||||
|
const apiKeyDataMap = new Map();
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const match = key.match(/usage:daily:(.+?):\d{4}-\d{2}-\d{2}/);
|
const match = key.match(/usage:daily:(.+?):\d{4}-\d{2}-\d{2}/);
|
||||||
if (!match) continue;
|
if (!match) continue;
|
||||||
@@ -2049,25 +2102,74 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0;
|
const cacheReadTokens = parseInt(data.cacheReadTokens) || 0;
|
||||||
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens;
|
||||||
|
|
||||||
// 计算费用 - 使用默认模型价格,因为日级别的汇总数据没有模型信息
|
apiKeyDataMap.set(apiKeyId, {
|
||||||
const usage = {
|
|
||||||
input_tokens: inputTokens,
|
|
||||||
output_tokens: outputTokens,
|
|
||||||
cache_creation_input_tokens: cacheCreateTokens,
|
|
||||||
cache_read_input_tokens: cacheReadTokens
|
|
||||||
};
|
|
||||||
const costResult = CostCalculator.calculateCost(usage, 'claude-3-5-haiku-20241022');
|
|
||||||
|
|
||||||
dayData.apiKeys[apiKeyId] = {
|
|
||||||
name: apiKeyMap.get(apiKeyId).name,
|
name: apiKeyMap.get(apiKeyId).name,
|
||||||
tokens: totalTokens,
|
tokens: totalTokens,
|
||||||
requests: parseInt(data.requests) || 0,
|
requests: parseInt(data.requests) || 0,
|
||||||
cost: costResult.costs.total,
|
inputTokens,
|
||||||
formattedCost: costResult.formatted.total
|
outputTokens,
|
||||||
};
|
cacheCreateTokens,
|
||||||
|
cacheReadTokens
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取该天的模型级别数据来计算准确费用
|
||||||
|
const modelPattern = `usage:*:model:daily:*:${dateStr}`;
|
||||||
|
const modelKeys = await client.keys(modelPattern);
|
||||||
|
const apiKeyCostMap = new Map();
|
||||||
|
|
||||||
|
for (const modelKey of modelKeys) {
|
||||||
|
const match = modelKey.match(/usage:(.+?):model:daily:(.+?):\d{4}-\d{2}-\d{2}/);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const apiKeyId = match[1];
|
||||||
|
const model = match[2];
|
||||||
|
const modelData = await client.hgetall(modelKey);
|
||||||
|
|
||||||
|
if (modelData && apiKeyDataMap.has(apiKeyId)) {
|
||||||
|
const usage = {
|
||||||
|
input_tokens: parseInt(modelData.inputTokens) || 0,
|
||||||
|
output_tokens: parseInt(modelData.outputTokens) || 0,
|
||||||
|
cache_creation_input_tokens: parseInt(modelData.cacheCreateTokens) || 0,
|
||||||
|
cache_read_input_tokens: parseInt(modelData.cacheReadTokens) || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const costResult = CostCalculator.calculateCost(usage, model);
|
||||||
|
const currentCost = apiKeyCostMap.get(apiKeyId) || 0;
|
||||||
|
apiKeyCostMap.set(apiKeyId, currentCost + costResult.costs.total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组合数据
|
||||||
|
for (const [apiKeyId, data] of apiKeyDataMap) {
|
||||||
|
const cost = apiKeyCostMap.get(apiKeyId) || 0;
|
||||||
|
|
||||||
|
// 如果没有模型级别数据,使用默认模型计算(降级方案)
|
||||||
|
let finalCost = cost;
|
||||||
|
let formattedCost = CostCalculator.formatCost(cost);
|
||||||
|
|
||||||
|
if (cost === 0 && data.tokens > 0) {
|
||||||
|
const usage = {
|
||||||
|
input_tokens: data.inputTokens,
|
||||||
|
output_tokens: data.outputTokens,
|
||||||
|
cache_creation_input_tokens: data.cacheCreateTokens,
|
||||||
|
cache_read_input_tokens: data.cacheReadTokens
|
||||||
|
};
|
||||||
|
const fallbackResult = CostCalculator.calculateCost(usage, 'claude-3-5-sonnet-20241022');
|
||||||
|
finalCost = fallbackResult.costs.total;
|
||||||
|
formattedCost = fallbackResult.formatted.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
dayData.apiKeys[apiKeyId] = {
|
||||||
|
name: data.name,
|
||||||
|
tokens: data.tokens,
|
||||||
|
requests: data.requests,
|
||||||
|
cost: finalCost,
|
||||||
|
formattedCost: formattedCost
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
trendData.push(dayData);
|
trendData.push(dayData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user