Merge pull request #801 from miraserver/fix/cost-calculation-and-ui-display

fix: correct API key cost calculation and UI display issues
This commit is contained in:
Wesley Liddick
2025-12-14 20:48:00 -05:00
committed by GitHub
3 changed files with 87 additions and 52 deletions

View File

@@ -945,6 +945,30 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) {
allTimeCost = parseFloat((await client.get(totalCostKey)) || '0') allTimeCost = parseFloat((await client.get(totalCostKey)) || '0')
} }
// 🔧 FIX: 对于 "全部时间" 时间范围,直接使用 allTimeCost
// 因为 usage:*:model:daily:* 键有 30 天 TTL旧数据已经过期
if (timeRange === 'all' && allTimeCost > 0) {
logger.debug(`📊 使用 allTimeCost 计算 timeRange='all': ${allTimeCost}`)
return {
requests: 0, // 旧数据详情不可用
tokens: 0,
inputTokens: 0,
outputTokens: 0,
cacheCreateTokens: 0,
cacheReadTokens: 0,
cost: allTimeCost,
formattedCost: CostCalculator.formatCost(allTimeCost),
// 实时限制数据(始终返回,不受时间范围影响)
dailyCost,
currentWindowCost,
windowRemainingSeconds,
windowStartTime,
windowEndTime,
allTimeCost
}
}
// 只在启用了窗口限制时查询窗口数据 // 只在启用了窗口限制时查询窗口数据
if (rateLimitWindow > 0) { if (rateLimitWindow > 0) {
const costCountKey = `rate_limit:cost:${keyId}` const costCountKey = `rate_limit:cost:${keyId}`

View File

@@ -206,74 +206,85 @@ router.post('/api/user-stats', async (req, res) => {
// 获取验证结果中的完整keyData包含isActive状态和cost信息 // 获取验证结果中的完整keyData包含isActive状态和cost信息
const fullKeyData = keyData const fullKeyData = keyData
// 计算总费用 - 使用与模型统计相同的逻辑(按模型分别计算) // 🔧 FIX: 使用 allTimeCost 而不是扫描月度键
// 计算总费用 - 优先使用持久化的总费用计数器
let totalCost = 0 let totalCost = 0
let formattedCost = '$0.000000' let formattedCost = '$0.000000'
try { try {
const client = redis.getClientSafe() const client = redis.getClientSafe()
// 获取所有月度模型统计与model-stats接口相同的逻辑 // 读取累积的总费用(没有 TTL 的持久键
const allModelKeys = await client.keys(`usage:${keyId}:model:monthly:*:*`) const totalCostKey = `usage:cost:total:${keyId}`
const modelUsageMap = new Map() const allTimeCost = parseFloat((await client.get(totalCostKey)) || '0')
for (const key of allModelKeys) { if (allTimeCost > 0) {
const modelMatch = key.match(/usage:.+:model:monthly:(.+):(\d{4}-\d{2})$/) totalCost = allTimeCost
if (!modelMatch) { formattedCost = CostCalculator.formatCost(allTimeCost)
continue logger.debug(`📊 使用 allTimeCost 计算用户统计: ${allTimeCost}`)
} } else {
// Fallback: 如果 allTimeCost 为空(旧键),尝试月度键
const allModelKeys = await client.keys(`usage:${keyId}:model:monthly:*:*`)
const modelUsageMap = new Map()
const model = modelMatch[1] for (const key of allModelKeys) {
const data = await client.hgetall(key) const modelMatch = key.match(/usage:.+:model:monthly:(.+):(\d{4}-\d{2})$/)
if (!modelMatch) {
if (data && Object.keys(data).length > 0) { continue
if (!modelUsageMap.has(model)) {
modelUsageMap.set(model, {
inputTokens: 0,
outputTokens: 0,
cacheCreateTokens: 0,
cacheReadTokens: 0
})
} }
const modelUsage = modelUsageMap.get(model) const model = modelMatch[1]
modelUsage.inputTokens += parseInt(data.inputTokens) || 0 const data = await client.hgetall(key)
modelUsage.outputTokens += parseInt(data.outputTokens) || 0
modelUsage.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0
modelUsage.cacheReadTokens += parseInt(data.cacheReadTokens) || 0
}
}
// 按模型计算费用并汇总 if (data && Object.keys(data).length > 0) {
for (const [model, usage] of modelUsageMap) { if (!modelUsageMap.has(model)) {
const usageData = { modelUsageMap.set(model, {
input_tokens: usage.inputTokens, inputTokens: 0,
output_tokens: usage.outputTokens, outputTokens: 0,
cache_creation_input_tokens: usage.cacheCreateTokens, cacheCreateTokens: 0,
cache_read_input_tokens: usage.cacheReadTokens cacheReadTokens: 0
})
}
const modelUsage = modelUsageMap.get(model)
modelUsage.inputTokens += parseInt(data.inputTokens) || 0
modelUsage.outputTokens += parseInt(data.outputTokens) || 0
modelUsage.cacheCreateTokens += parseInt(data.cacheCreateTokens) || 0
modelUsage.cacheReadTokens += parseInt(data.cacheReadTokens) || 0
}
} }
const costResult = CostCalculator.calculateCost(usageData, model) // 按模型计算费用并汇总
totalCost += costResult.costs.total for (const [model, usage] of modelUsageMap) {
} const usageData = {
input_tokens: usage.inputTokens,
output_tokens: usage.outputTokens,
cache_creation_input_tokens: usage.cacheCreateTokens,
cache_read_input_tokens: usage.cacheReadTokens
}
// 如果没有模型级别的详细数据,回退到总体数据计算 const costResult = CostCalculator.calculateCost(usageData, model)
if (modelUsageMap.size === 0 && fullKeyData.usage?.total?.allTokens > 0) { totalCost += costResult.costs.total
const usage = fullKeyData.usage.total
const costUsage = {
input_tokens: usage.inputTokens || 0,
output_tokens: usage.outputTokens || 0,
cache_creation_input_tokens: usage.cacheCreateTokens || 0,
cache_read_input_tokens: usage.cacheReadTokens || 0
} }
const costResult = CostCalculator.calculateCost(costUsage, 'claude-3-5-sonnet-20241022') // 如果没有模型级别的详细数据,回退到总体数据计算
totalCost = costResult.costs.total if (modelUsageMap.size === 0 && fullKeyData.usage?.total?.allTokens > 0) {
} const usage = fullKeyData.usage.total
const costUsage = {
input_tokens: usage.inputTokens || 0,
output_tokens: usage.outputTokens || 0,
cache_creation_input_tokens: usage.cacheCreateTokens || 0,
cache_read_input_tokens: usage.cacheReadTokens || 0
}
formattedCost = CostCalculator.formatCost(totalCost) const costResult = CostCalculator.calculateCost(costUsage, 'claude-3-5-sonnet-20241022')
totalCost = costResult.costs.total
}
formattedCost = CostCalculator.formatCost(totalCost)
}
} catch (error) { } catch (error) {
logger.warn(`Failed to calculate detailed cost for key ${keyId}:`, error) logger.warn(`Failed to calculate cost for key ${keyId}:`, error)
// 回退到简单计算 // 回退到简单计算
if (fullKeyData.usage?.total?.allTokens > 0) { if (fullKeyData.usage?.total?.allTokens > 0) {
const usage = fullKeyData.usage.total const usage = fullKeyData.usage.total

View File

@@ -284,7 +284,7 @@
</div> </div>
<!-- 详细限制信息 --> <!-- 详细限制信息 -->
<div v-if="hasModelRestrictions" class="card p-4 md:p-6"> <div v-if="hasModelRestrictions" class="card !overflow-visible p-4 md:p-6">
<h3 <h3
class="mb-3 flex items-center text-lg font-bold text-gray-900 dark:text-gray-100 md:mb-4 md:text-xl" class="mb-3 flex items-center text-lg font-bold text-gray-900 dark:text-gray-100 md:mb-4 md:text-xl"
> >
@@ -301,7 +301,7 @@
<i class="fas fa-robot mr-1 text-xs md:mr-2 md:text-sm" /> <i class="fas fa-robot mr-1 text-xs md:mr-2 md:text-sm" />
受限模型列表 受限模型列表
</h4> </h4>
<div class="space-y-1 md:space-y-2"> <div class="max-h-64 space-y-1 overflow-y-auto pr-1 md:max-h-80 md:space-y-2">
<div <div
v-for="model in statsData.restrictions.restrictedModels" v-for="model in statsData.restrictions.restrictedModels"
:key="model" :key="model"