feat: add model name normalization for statistics aggregation in Redis and admin routes

This commit is contained in:
andersonby
2025-08-07 01:35:01 +08:00
parent 622c4047e9
commit 7ea899bd30
3 changed files with 102 additions and 16 deletions

View File

@@ -164,6 +164,24 @@ class RedisClient {
}
// 📊 使用统计相关操作支持缓存token统计和模型信息
// 标准化模型名称,用于统计聚合
_normalizeModelName(model) {
if (!model || model === 'unknown') return model;
// 对于Bedrock模型去掉区域前缀进行统一
if (model.includes('.anthropic.') || model.includes('.claude')) {
// 匹配所有AWS区域格式region.anthropic.model-name-v1:0 -> claude-model-name
// 支持所有AWS区域格式us-east-1, eu-west-1, ap-southeast-1, ca-central-1等
let normalized = model.replace(/^[a-z0-9-]+\./, ''); // 去掉任何区域前缀(更通用)
normalized = normalized.replace('anthropic.', ''); // 去掉anthropic前缀
normalized = normalized.replace(/-v\d+:\d+$/, ''); // 去掉版本后缀(如-v1:0, -v2:1等
return normalized;
}
// 对于其他模型,去掉常见的版本后缀
return model.replace(/-v\d+:\d+$|:latest$/, '');
}
async incrementTokenUsage(keyId, tokens, inputTokens = 0, outputTokens = 0, cacheCreateTokens = 0, cacheReadTokens = 0, model = 'unknown') {
const key = `usage:${keyId}`;
const now = new Date();
@@ -176,15 +194,18 @@ class RedisClient {
const monthly = `usage:monthly:${keyId}:${currentMonth}`;
const hourly = `usage:hourly:${keyId}:${currentHour}`; // 新增小时级别key
// 标准化模型名用于统计聚合
const normalizedModel = this._normalizeModelName(model);
// 按模型统计的键
const modelDaily = `usage:model:daily:${model}:${today}`;
const modelMonthly = `usage:model:monthly:${model}:${currentMonth}`;
const modelHourly = `usage:model:hourly:${model}:${currentHour}`; // 新增模型小时级别
const modelDaily = `usage:model:daily:${normalizedModel}:${today}`;
const modelMonthly = `usage:model:monthly:${normalizedModel}:${currentMonth}`;
const modelHourly = `usage:model:hourly:${normalizedModel}:${currentHour}`; // 新增模型小时级别
// API Key级别的模型统计
const keyModelDaily = `usage:${keyId}:model:daily:${model}:${today}`;
const keyModelMonthly = `usage:${keyId}:model:monthly:${model}:${currentMonth}`;
const keyModelHourly = `usage:${keyId}:model:hourly:${model}:${currentHour}`; // 新增API Key模型小时级别
const keyModelDaily = `usage:${keyId}:model:daily:${normalizedModel}:${today}`;
const keyModelMonthly = `usage:${keyId}:model:monthly:${normalizedModel}:${currentMonth}`;
const keyModelHourly = `usage:${keyId}:model:hourly:${normalizedModel}:${currentHour}`; // 新增API Key模型小时级别
// 新增:系统级分钟统计
const minuteTimestamp = Math.floor(now.getTime() / 60000);
@@ -333,10 +354,13 @@ class RedisClient {
const accountMonthly = `account_usage:monthly:${accountId}:${currentMonth}`;
const accountHourly = `account_usage:hourly:${accountId}:${currentHour}`;
// 标准化模型名用于统计聚合
const normalizedModel = this._normalizeModelName(model);
// 账户按模型统计的键
const accountModelDaily = `account_usage:model:daily:${accountId}:${model}:${today}`;
const accountModelMonthly = `account_usage:model:monthly:${accountId}:${model}:${currentMonth}`;
const accountModelHourly = `account_usage:model:hourly:${accountId}:${model}:${currentHour}`;
const accountModelDaily = `account_usage:model:daily:${accountId}:${normalizedModel}:${today}`;
const accountModelMonthly = `account_usage:model:monthly:${accountId}:${normalizedModel}:${currentMonth}`;
const accountModelHourly = `account_usage:model:hourly:${accountId}:${normalizedModel}:${currentHour}`;
// 处理token分配
const finalInputTokens = inputTokens || 0;

View File

@@ -2095,6 +2095,24 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => {
logger.info(`📊 Found ${allKeys.length} matching keys in total`);
// 模型名标准化函数与redis.js保持一致
const normalizeModelName = (model) => {
if (!model || model === 'unknown') return model;
// 对于Bedrock模型去掉区域前缀进行统一
if (model.includes('.anthropic.') || model.includes('.claude')) {
// 匹配所有AWS区域格式region.anthropic.model-name-v1:0 -> claude-model-name
// 支持所有AWS区域格式us-east-1, eu-west-1, ap-southeast-1, ca-central-1等
let normalized = model.replace(/^[a-z0-9-]+\./, ''); // 去掉任何区域前缀(更通用)
normalized = normalized.replace('anthropic.', ''); // 去掉anthropic前缀
normalized = normalized.replace(/-v\d+:\d+$/, ''); // 去掉版本后缀(如-v1:0, -v2:1等
return normalized;
}
// 对于其他模型,去掉常见的版本后缀
return model.replace(/-v\d+:\d+$|:latest$/, '');
};
// 聚合相同模型的数据
const modelStatsMap = new Map();
@@ -2106,11 +2124,12 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => {
continue;
}
const model = match[1];
const rawModel = match[1];
const normalizedModel = normalizeModelName(rawModel);
const data = await client.hgetall(key);
if (data && Object.keys(data).length > 0) {
const stats = modelStatsMap.get(model) || {
const stats = modelStatsMap.get(normalizedModel) || {
requests: 0,
inputTokens: 0,
outputTokens: 0,
@@ -2126,7 +2145,7 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => {
stats.cacheReadTokens += parseInt(data.cacheReadTokens) || 0;
stats.allTokens += parseInt(data.allTokens) || 0;
modelStatsMap.set(model, stats);
modelStatsMap.set(normalizedModel, stats);
}
}
@@ -2950,6 +2969,24 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
logger.info(`💰 Calculating usage costs for period: ${period}`);
// 模型名标准化函数与redis.js保持一致
const normalizeModelName = (model) => {
if (!model || model === 'unknown') return model;
// 对于Bedrock模型去掉区域前缀进行统一
if (model.includes('.anthropic.') || model.includes('.claude')) {
// 匹配所有AWS区域格式region.anthropic.model-name-v1:0 -> claude-model-name
// 支持所有AWS区域格式us-east-1, eu-west-1, ap-southeast-1, ca-central-1等
let normalized = model.replace(/^[a-z0-9-]+\./, ''); // 去掉任何区域前缀(更通用)
normalized = normalized.replace('anthropic.', ''); // 去掉anthropic前缀
normalized = normalized.replace(/-v\d+:\d+$/, ''); // 去掉版本后缀(如-v1:0, -v2:1等
return normalized;
}
// 对于其他模型,去掉常见的版本后缀
return model.replace(/-v\d+:\d+$|:latest$/, '');
};
// 获取所有API Keys的使用统计
const apiKeys = await apiKeyService.getAllApiKeys();
@@ -2992,12 +3029,13 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
const modelMatch = key.match(/usage:model:daily:(.+):\d{4}-\d{2}-\d{2}$/);
if (!modelMatch) continue;
const model = modelMatch[1];
const rawModel = modelMatch[1];
const normalizedModel = normalizeModelName(rawModel);
const data = await client.hgetall(key);
if (data && Object.keys(data).length > 0) {
if (!modelUsageMap.has(model)) {
modelUsageMap.set(model, {
if (!modelUsageMap.has(normalizedModel)) {
modelUsageMap.set(normalizedModel, {
inputTokens: 0,
outputTokens: 0,
cacheCreateTokens: 0,
@@ -3005,7 +3043,7 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
});
}
const modelUsage = modelUsageMap.get(model);
const modelUsage = modelUsageMap.get(normalizedModel);
modelUsage.inputTokens += parseInt(data.inputTokens) || 0;
modelUsage.outputTokens += parseInt(data.outputTokens) || 0;
modelUsage.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0;

View File

@@ -203,6 +203,17 @@ class PricingService {
return this.pricingData[modelName];
}
// 对于Bedrock区域前缀模型如 us.anthropic.claude-sonnet-4-20250514-v1:0
// 尝试去掉区域前缀进行匹配
if (modelName.includes('.anthropic.') || modelName.includes('.claude')) {
// 提取不带区域前缀的模型名
const withoutRegion = modelName.replace(/^(us|eu|apac)\./, '');
if (this.pricingData[withoutRegion]) {
logger.debug(`💰 Found pricing for ${modelName} by removing region prefix: ${withoutRegion}`);
return this.pricingData[withoutRegion];
}
}
// 尝试模糊匹配(处理版本号等变化)
const normalizedModel = modelName.toLowerCase().replace(/[_-]/g, '');
@@ -214,6 +225,19 @@ class PricingService {
}
}
// 对于Bedrock模型尝试更智能的匹配
if (modelName.includes('anthropic.claude')) {
// 提取核心模型名部分(去掉区域和前缀)
const coreModel = modelName.replace(/^(us|eu|apac)\./, '').replace('anthropic.', '');
for (const [key, value] of Object.entries(this.pricingData)) {
if (key.includes(coreModel) || key.replace('anthropic.', '').includes(coreModel)) {
logger.debug(`💰 Found pricing for ${modelName} using Bedrock core model match: ${key}`);
return value;
}
}
}
logger.debug(`💰 No pricing found for model: ${modelName}`);
return null;
}