diff --git a/src/models/redis.js b/src/models/redis.js index 5d934750..b27fdf9c 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -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; diff --git a/src/routes/admin.js b/src/routes/admin.js index 643fb2e4..01c75810 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -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; diff --git a/src/services/pricingService.js b/src/services/pricingService.js index 6d8b586d..be4ef21c 100644 --- a/src/services/pricingService.js +++ b/src/services/pricingService.js @@ -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; }