From 9a46310238e4b67a78aabf24eb0913d578f80489 Mon Sep 17 00:00:00 2001 From: shaw Date: Sun, 31 Aug 2025 20:14:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E4=BD=BF=E7=94=A8=E7=BB=9F=E8=AE=A1=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/redis.js | 48 +++++++++++++++++++++++++++------- src/routes/admin.js | 3 +++ src/services/pricingService.js | 16 ++++++++++++ src/utils/costCalculator.js | 8 ++++++ 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/models/redis.js b/src/models/redis.js index 7db5b280..b65cc8d1 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -1431,7 +1431,13 @@ class RedisClient { const startDate = new Date(windowStart) const endDate = new Date(windowEnd) + // 添加日志以调试时间窗口 + logger.debug(`📊 Getting session window usage for account ${accountId}`) + logger.debug(` Window: ${windowStart} to ${windowEnd}`) + logger.debug(` Start UTC: ${startDate.toISOString()}, End UTC: ${endDate.toISOString()}`) + // 获取窗口内所有可能的小时键 + // 重要:需要使用配置的时区来构建键名,因为数据存储时使用的是配置时区 const hourlyKeys = [] const currentHour = new Date(startDate) currentHour.setMinutes(0) @@ -1439,9 +1445,12 @@ class RedisClient { currentHour.setMilliseconds(0) while (currentHour <= endDate) { - const dateStr = `${currentHour.getUTCFullYear()}-${String(currentHour.getUTCMonth() + 1).padStart(2, '0')}-${String(currentHour.getUTCDate()).padStart(2, '0')}` - const hourStr = String(currentHour.getUTCHours()).padStart(2, '0') - const key = `account_usage:hourly:${accountId}:${dateStr}:${hourStr}` + // 使用时区转换函数来获取正确的日期和小时 + const tzDateStr = getDateStringInTimezone(currentHour) + const tzHour = String(getHourInTimezone(currentHour)).padStart(2, '0') + const key = `account_usage:hourly:${accountId}:${tzDateStr}:${tzHour}` + + logger.debug(` Adding hourly key: ${key}`) hourlyKeys.push(key) currentHour.setHours(currentHour.getHours() + 1) } @@ -1462,18 +1471,31 @@ class RedisClient { let totalRequests = 0 const modelUsage = {} + logger.debug(` Processing ${results.length} hourly results`) + for (const [error, data] of results) { if (error || !data || Object.keys(data).length === 0) { continue } // 处理总计数据 - totalInputTokens += parseInt(data.inputTokens || 0) - totalOutputTokens += parseInt(data.outputTokens || 0) - totalCacheCreateTokens += parseInt(data.cacheCreateTokens || 0) - totalCacheReadTokens += parseInt(data.cacheReadTokens || 0) - totalAllTokens += parseInt(data.allTokens || 0) - totalRequests += parseInt(data.requests || 0) + const hourInputTokens = parseInt(data.inputTokens || 0) + const hourOutputTokens = parseInt(data.outputTokens || 0) + const hourCacheCreateTokens = parseInt(data.cacheCreateTokens || 0) + const hourCacheReadTokens = parseInt(data.cacheReadTokens || 0) + const hourAllTokens = parseInt(data.allTokens || 0) + const hourRequests = parseInt(data.requests || 0) + + totalInputTokens += hourInputTokens + totalOutputTokens += hourOutputTokens + totalCacheCreateTokens += hourCacheCreateTokens + totalCacheReadTokens += hourCacheReadTokens + totalAllTokens += hourAllTokens + totalRequests += hourRequests + + if (hourAllTokens > 0) { + logger.debug(` Hour data: allTokens=${hourAllTokens}, requests=${hourRequests}`) + } // 处理每个模型的数据 for (const [key, value] of Object.entries(data)) { @@ -1513,6 +1535,14 @@ class RedisClient { } } + logger.debug(`📊 Session window usage summary:`) + logger.debug(` Total allTokens: ${totalAllTokens}`) + logger.debug(` Total requests: ${totalRequests}`) + logger.debug(` Input: ${totalInputTokens}, Output: ${totalOutputTokens}`) + logger.debug( + ` Cache Create: ${totalCacheCreateTokens}, Cache Read: ${totalCacheReadTokens}` + ) + return { totalInputTokens, totalOutputTokens, diff --git a/src/routes/admin.js b/src/routes/admin.js index 1436a6c0..478669d1 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -1524,7 +1524,10 @@ router.get('/claude-accounts', authenticateAdmin, async (req, res) => { cache_read_input_tokens: usage.cacheReadTokens } + logger.debug(`💰 Calculating cost for model ${modelName}:`, JSON.stringify(usageData)) const costResult = CostCalculator.calculateCost(usageData, modelName) + logger.debug(`💰 Cost result for ${modelName}: total=${costResult.costs.total}`) + modelCosts[modelName] = { ...usage, cost: costResult.costs.total diff --git a/src/services/pricingService.js b/src/services/pricingService.js index 0084a2ab..606465d5 100644 --- a/src/services/pricingService.js +++ b/src/services/pricingService.js @@ -45,6 +45,7 @@ class PricingService { 'claude-sonnet-3-5': 0.000006, 'claude-sonnet-3-7': 0.000006, 'claude-sonnet-4': 0.000006, + 'claude-sonnet-4-20250514': 0.000006, // Haiku 系列: $1.6/MTok 'claude-3-5-haiku': 0.0000016, @@ -260,6 +261,7 @@ class PricingService { // 尝试直接匹配 if (this.pricingData[modelName]) { + logger.debug(`💰 Found exact pricing match for ${modelName}`) return this.pricingData[modelName] } @@ -304,6 +306,20 @@ class PricingService { return null } + // 确保价格对象包含缓存价格 + ensureCachePricing(pricing) { + if (!pricing) return pricing + + // 如果缺少缓存价格,根据输入价格计算(缓存创建价格通常是输入价格的1.25倍,缓存读取是0.1倍) + if (!pricing.cache_creation_input_token_cost && pricing.input_cost_per_token) { + pricing.cache_creation_input_token_cost = pricing.input_cost_per_token * 1.25 + } + if (!pricing.cache_read_input_token_cost && pricing.input_cost_per_token) { + pricing.cache_read_input_token_cost = pricing.input_cost_per_token * 0.1 + } + return pricing + } + // 获取 1 小时缓存价格 getEphemeral1hPricing(modelName) { if (!modelName) { diff --git a/src/utils/costCalculator.js b/src/utils/costCalculator.js index 3c3b7c41..e623abaa 100644 --- a/src/utils/costCalculator.js +++ b/src/utils/costCalculator.js @@ -31,6 +31,14 @@ const MODEL_PRICING = { cacheWrite: 18.75, cacheRead: 1.5 }, + + // Claude Opus 4.1 (新模型) + 'claude-opus-4-1-20250805': { + input: 15.0, + output: 75.0, + cacheWrite: 18.75, + cacheRead: 1.5 + }, // Claude 3 Sonnet 'claude-3-sonnet-20240229': {