diff --git a/.gitignore b/.gitignore index e4c9e9c1..71121e38 100644 --- a/.gitignore +++ b/.gitignore @@ -247,3 +247,5 @@ web/apiStats/ # Admin SPA build files web/admin-spa/dist/ + +.serena/ diff --git a/src/models/redis.js b/src/models/redis.js index 2f139d6a..dbac2a7d 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -1084,6 +1084,9 @@ class RedisClient { pipeline.hincrby(modelDaily, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(modelDaily, 'allTokens', totalTokens) pipeline.hincrby(modelDaily, 'requests', 1) + // 详细缓存类型统计 + pipeline.hincrby(modelDaily, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(modelDaily, 'ephemeral1hTokens', ephemeral1hTokens) // 按模型统计 - 每月 pipeline.hincrby(modelMonthly, 'inputTokens', finalInputTokens) @@ -1092,6 +1095,9 @@ class RedisClient { pipeline.hincrby(modelMonthly, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(modelMonthly, 'allTokens', totalTokens) pipeline.hincrby(modelMonthly, 'requests', 1) + // 详细缓存类型统计 + pipeline.hincrby(modelMonthly, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(modelMonthly, 'ephemeral1hTokens', ephemeral1hTokens) // API Key级别的模型统计 - 每日 pipeline.hincrby(keyModelDaily, 'inputTokens', finalInputTokens) @@ -1136,6 +1142,9 @@ class RedisClient { pipeline.hincrby(keyModelAlltime, 'cacheCreateTokens', finalCacheCreateTokens) pipeline.hincrby(keyModelAlltime, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(keyModelAlltime, 'requests', 1) + // 详细缓存类型统计 + pipeline.hincrby(keyModelAlltime, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(keyModelAlltime, 'ephemeral1hTokens', ephemeral1hTokens) // 费用统计 if (realCost > 0) { pipeline.hincrby(keyModelAlltime, 'realCostMicro', Math.round(realCost * 1000000)) @@ -1152,6 +1161,9 @@ class RedisClient { pipeline.hincrby(hourly, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(hourly, 'allTokens', totalTokens) pipeline.hincrby(hourly, 'requests', 1) + // 详细缓存类型统计 + pipeline.hincrby(hourly, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(hourly, 'ephemeral1hTokens', ephemeral1hTokens) // 按模型统计 - 每小时 pipeline.hincrby(modelHourly, 'inputTokens', finalInputTokens) @@ -1160,6 +1172,9 @@ class RedisClient { pipeline.hincrby(modelHourly, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(modelHourly, 'allTokens', totalTokens) pipeline.hincrby(modelHourly, 'requests', 1) + // 详细缓存类型统计 + pipeline.hincrby(modelHourly, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(modelHourly, 'ephemeral1hTokens', ephemeral1hTokens) // API Key级别的模型统计 - 每小时 pipeline.hincrby(keyModelHourly, 'inputTokens', finalInputTokens) @@ -1168,6 +1183,9 @@ class RedisClient { pipeline.hincrby(keyModelHourly, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(keyModelHourly, 'allTokens', totalTokens) pipeline.hincrby(keyModelHourly, 'requests', 1) + // 详细缓存类型统计 + pipeline.hincrby(keyModelHourly, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(keyModelHourly, 'ephemeral1hTokens', ephemeral1hTokens) // 费用统计 if (realCost > 0) { pipeline.hincrby(keyModelHourly, 'realCostMicro', Math.round(realCost * 1000000)) @@ -1235,18 +1253,24 @@ class RedisClient { pipeline.hincrby('usage:global:total', 'cacheCreateTokens', finalCacheCreateTokens) pipeline.hincrby('usage:global:total', 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby('usage:global:total', 'allTokens', totalTokens) + pipeline.hincrby('usage:global:total', 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby('usage:global:total', 'ephemeral1hTokens', ephemeral1hTokens) pipeline.hincrby(globalDaily, 'requests', 1) pipeline.hincrby(globalDaily, 'inputTokens', finalInputTokens) pipeline.hincrby(globalDaily, 'outputTokens', finalOutputTokens) pipeline.hincrby(globalDaily, 'cacheCreateTokens', finalCacheCreateTokens) pipeline.hincrby(globalDaily, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(globalDaily, 'allTokens', totalTokens) + pipeline.hincrby(globalDaily, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(globalDaily, 'ephemeral1hTokens', ephemeral1hTokens) pipeline.hincrby(globalMonthly, 'requests', 1) pipeline.hincrby(globalMonthly, 'inputTokens', finalInputTokens) pipeline.hincrby(globalMonthly, 'outputTokens', finalOutputTokens) pipeline.hincrby(globalMonthly, 'cacheCreateTokens', finalCacheCreateTokens) pipeline.hincrby(globalMonthly, 'cacheReadTokens', finalCacheReadTokens) pipeline.hincrby(globalMonthly, 'allTokens', totalTokens) + pipeline.hincrby(globalMonthly, 'ephemeral5mTokens', ephemeral5mTokens) + pipeline.hincrby(globalMonthly, 'ephemeral1hTokens', ephemeral1hTokens) pipeline.expire(globalDaily, 86400 * 32) pipeline.expire(globalMonthly, 86400 * 365) diff --git a/src/routes/admin/apiKeys.js b/src/routes/admin/apiKeys.js index 7bbc4f8a..d348493c 100644 --- a/src/routes/admin/apiKeys.js +++ b/src/routes/admin/apiKeys.js @@ -1289,6 +1289,8 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) { outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 0, + ephemeral5mTokens: 0, + ephemeral1hTokens: 0, requests: 0 }) } @@ -1300,6 +1302,10 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) { parseInt(data.totalCacheCreateTokens) || parseInt(data.cacheCreateTokens) || 0 stats.cacheReadTokens += parseInt(data.totalCacheReadTokens) || parseInt(data.cacheReadTokens) || 0 + stats.ephemeral5mTokens += + parseInt(data.totalEphemeral5mTokens) || parseInt(data.ephemeral5mTokens) || 0 + stats.ephemeral1hTokens += + parseInt(data.totalEphemeral1hTokens) || parseInt(data.ephemeral1hTokens) || 0 stats.requests += parseInt(data.totalRequests) || parseInt(data.requests) || 0 totalRequests += parseInt(data.totalRequests) || parseInt(data.requests) || 0 @@ -1318,15 +1324,22 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) { cacheCreateTokens += stats.cacheCreateTokens cacheReadTokens += stats.cacheReadTokens - const costResult = CostCalculator.calculateCost( - { - input_tokens: stats.inputTokens, - output_tokens: stats.outputTokens, - cache_creation_input_tokens: stats.cacheCreateTokens, - cache_read_input_tokens: stats.cacheReadTokens - }, - model - ) + const costUsage = { + input_tokens: stats.inputTokens, + output_tokens: stats.outputTokens, + cache_creation_input_tokens: stats.cacheCreateTokens, + cache_read_input_tokens: stats.cacheReadTokens + } + + // 如果有 ephemeral 5m/1h 拆分数据,添加 cache_creation 子对象以实现精确计费 + if (stats.ephemeral5mTokens > 0 || stats.ephemeral1hTokens > 0) { + costUsage.cache_creation = { + ephemeral_5m_input_tokens: stats.ephemeral5mTokens, + ephemeral_1h_input_tokens: stats.ephemeral1hTokens + } + } + + const costResult = CostCalculator.calculateCost(costUsage, model) totalCost += costResult.costs.total } diff --git a/src/routes/admin/dashboard.js b/src/routes/admin/dashboard.js index fb47f98e..52f7f09e 100644 --- a/src/routes/admin/dashboard.js +++ b/src/routes/admin/dashboard.js @@ -472,7 +472,9 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => { outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 0, - allTokens: 0 + allTokens: 0, + ephemeral5mTokens: 0, + ephemeral1hTokens: 0 } stats.requests += parseInt(data.requests) || 0 @@ -481,6 +483,8 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => { stats.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0 stats.cacheReadTokens += parseInt(data.cacheReadTokens) || 0 stats.allTokens += parseInt(data.allTokens) || 0 + stats.ephemeral5mTokens += parseInt(data.ephemeral5mTokens) || 0 + stats.ephemeral1hTokens += parseInt(data.ephemeral1hTokens) || 0 modelStatsMap.set(normalizedModel, stats) } @@ -497,6 +501,14 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => { cache_read_input_tokens: stats.cacheReadTokens } + // 如果有 ephemeral 5m/1h 拆分数据,添加 cache_creation 子对象以实现精确计费 + if (stats.ephemeral5mTokens > 0 || stats.ephemeral1hTokens > 0) { + usage.cache_creation = { + ephemeral_5m_input_tokens: stats.ephemeral5mTokens, + ephemeral_1h_input_tokens: stats.ephemeral1hTokens + } + } + // 计算费用 const costData = CostCalculator.calculateCost(usage, model) diff --git a/src/routes/admin/usageStats.js b/src/routes/admin/usageStats.js index cfa61bd4..5bd9ea79 100644 --- a/src/routes/admin/usageStats.js +++ b/src/routes/admin/usageStats.js @@ -786,6 +786,8 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { const modelOutputTokens = parseInt(data.outputTokens) || 0 const modelCacheCreateTokens = parseInt(data.cacheCreateTokens) || 0 const modelCacheReadTokens = parseInt(data.cacheReadTokens) || 0 + const modelEphemeral5mTokens = parseInt(data.ephemeral5mTokens) || 0 + const modelEphemeral1hTokens = parseInt(data.ephemeral1hTokens) || 0 const modelRequests = parseInt(data.requests) || 0 dayInputTokens += modelInputTokens @@ -800,6 +802,15 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { cache_creation_input_tokens: modelCacheCreateTokens, cache_read_input_tokens: modelCacheReadTokens } + + // 如果有 ephemeral 5m/1h 拆分数据,添加 cache_creation 子对象以实现精确计费 + if (modelEphemeral5mTokens > 0 || modelEphemeral1hTokens > 0) { + modelUsage.cache_creation = { + ephemeral_5m_input_tokens: modelEphemeral5mTokens, + ephemeral_1h_input_tokens: modelEphemeral1hTokens + } + } + const modelCostResult = CostCalculator.calculateCost(modelUsage, model) dayCost += modelCostResult.costs.total } @@ -948,6 +959,8 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 0, + ephemeral5mTokens: 0, + ephemeral1hTokens: 0, allTokens: 0 }) } @@ -957,6 +970,8 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = stats.outputTokens += parseInt(data.outputTokens) || 0 stats.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0 stats.cacheReadTokens += parseInt(data.cacheReadTokens) || 0 + stats.ephemeral5mTokens += parseInt(data.ephemeral5mTokens) || 0 + stats.ephemeral1hTokens += parseInt(data.ephemeral1hTokens) || 0 stats.allTokens += parseInt(data.allTokens) || 0 } } @@ -992,6 +1007,8 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = outputTokens: 0, cacheCreateTokens: 0, cacheReadTokens: 0, + ephemeral5mTokens: 0, + ephemeral1hTokens: 0, allTokens: 0 }) } @@ -1001,6 +1018,8 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = stats.outputTokens += parseInt(data.outputTokens) || 0 stats.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0 stats.cacheReadTokens += parseInt(data.cacheReadTokens) || 0 + stats.ephemeral5mTokens += parseInt(data.ephemeral5mTokens) || 0 + stats.ephemeral1hTokens += parseInt(data.ephemeral1hTokens) || 0 stats.allTokens += parseInt(data.allTokens) || 0 } } @@ -1016,6 +1035,14 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = cache_read_input_tokens: stats.cacheReadTokens } + // 如果有 ephemeral 5m/1h 拆分数据,添加 cache_creation 子对象以实现精确计费 + if (stats.ephemeral5mTokens > 0 || stats.ephemeral1hTokens > 0) { + usage.cache_creation = { + ephemeral_5m_input_tokens: stats.ephemeral5mTokens, + ephemeral_1h_input_tokens: stats.ephemeral1hTokens + } + } + // 使用CostCalculator计算费用 const costData = CostCalculator.calculateCost(usage, model) @@ -1424,6 +1451,16 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => { cache_read_input_tokens: parseInt(modelData.cacheReadTokens) || 0 } + // 如果有 ephemeral 5m/1h 拆分数据,添加 cache_creation 子对象以实现精确计费 + const eph5m = parseInt(modelData.ephemeral5mTokens) || 0 + const eph1h = parseInt(modelData.ephemeral1hTokens) || 0 + if (eph5m > 0 || eph1h > 0) { + usage.cache_creation = { + ephemeral_5m_input_tokens: eph5m, + ephemeral_1h_input_tokens: eph1h + } + } + const costResult = CostCalculator.calculateCost(usage, modelName) cost += costResult.costs.total } @@ -1582,6 +1619,16 @@ router.get('/account-usage-trend', authenticateAdmin, async (req, res) => { cache_read_input_tokens: parseInt(modelData.cacheReadTokens) || 0 } + // 如果有 ephemeral 5m/1h 拆分数据,添加 cache_creation 子对象以实现精确计费 + const eph5m = parseInt(modelData.ephemeral5mTokens) || 0 + const eph1h = parseInt(modelData.ephemeral1hTokens) || 0 + if (eph5m > 0 || eph1h > 0) { + usage.cache_creation = { + ephemeral_5m_input_tokens: eph5m, + ephemeral_1h_input_tokens: eph1h + } + } + const costResult = CostCalculator.calculateCost(usage, modelName) cost += costResult.costs.total } diff --git a/src/routes/apiStats.js b/src/routes/apiStats.js index 9d7b2ca6..a156df65 100644 --- a/src/routes/apiStats.js +++ b/src/routes/apiStats.js @@ -270,7 +270,9 @@ router.post('/api/user-stats', async (req, res) => { inputTokens: 0, outputTokens: 0, cacheCreateTokens: 0, - cacheReadTokens: 0 + cacheReadTokens: 0, + ephemeral5mTokens: 0, + ephemeral1hTokens: 0 }) } @@ -279,6 +281,8 @@ router.post('/api/user-stats', async (req, res) => { modelUsage.outputTokens += parseInt(data.outputTokens) || 0 modelUsage.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0 modelUsage.cacheReadTokens += parseInt(data.cacheReadTokens) || 0 + modelUsage.ephemeral5mTokens += parseInt(data.ephemeral5mTokens) || 0 + modelUsage.ephemeral1hTokens += parseInt(data.ephemeral1hTokens) || 0 } } @@ -291,6 +295,14 @@ router.post('/api/user-stats', async (req, res) => { cache_read_input_tokens: usage.cacheReadTokens } + // 如果有 ephemeral 5m/1h 拆分数据,添加 cache_creation 子对象以实现精确计费 + if (usage.ephemeral5mTokens > 0 || usage.ephemeral1hTokens > 0) { + usageData.cache_creation = { + ephemeral_5m_input_tokens: usage.ephemeral5mTokens, + ephemeral_1h_input_tokens: usage.ephemeral1hTokens + } + } + const costResult = CostCalculator.calculateCost(usageData, model) totalCost += costResult.costs.total } diff --git a/src/services/accountBalanceService.js b/src/services/accountBalanceService.js index ec25f171..81fd0501 100644 --- a/src/services/accountBalanceService.js +++ b/src/services/accountBalanceService.js @@ -607,6 +607,16 @@ class AccountBalanceService { cache_read_input_tokens: parseInt(data.cacheReadTokens || 0) } + // 如果有 ephemeral 5m/1h 拆分数据,添加 cache_creation 子对象以实现精确计费 + const eph5m = parseInt(data.ephemeral5mTokens || 0) + const eph1h = parseInt(data.ephemeral1hTokens || 0) + if (eph5m > 0 || eph1h > 0) { + usage.cache_creation = { + ephemeral_5m_input_tokens: eph5m, + ephemeral_1h_input_tokens: eph1h + } + } + const costResult = CostCalculator.calculateCost(usage, model) totalCost += costResult.costs.total || 0 }