Files
claude-relay-service/src/utils/costCalculator.js
shaw 681cb8cd82 feat: 优化 Claude 模型缓存费用计算,支持 5 分钟和 1 小时两种缓存类型
- 在 pricingService 中硬编码 1 小时缓存价格(Opus: $30/MTok, Sonnet: $6/MTok, Haiku: $1.6/MTok)
- 更新 usage 捕获逻辑以分别记录 ephemeral_5m 和 ephemeral_1h 缓存 tokens
- 改进费用计算逻辑,正确计算两种缓存类型的费用
- 新增 recordUsageWithDetails 方法支持详细的缓存数据
- 保持向后兼容性,支持旧的数据格式
- 删除测试脚本 test-openai-refresh.js
- 修复 OpenAI token 刷新逻辑

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-16 22:31:20 +08:00

257 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const pricingService = require('../services/pricingService')
// Claude模型价格配置 (USD per 1M tokens) - 备用定价
const MODEL_PRICING = {
// Claude 3.5 Sonnet
'claude-3-5-sonnet-20241022': {
input: 3.0,
output: 15.0,
cacheWrite: 3.75,
cacheRead: 0.3
},
'claude-sonnet-4-20250514': {
input: 3.0,
output: 15.0,
cacheWrite: 3.75,
cacheRead: 0.3
},
// Claude 3.5 Haiku
'claude-3-5-haiku-20241022': {
input: 0.25,
output: 1.25,
cacheWrite: 0.3,
cacheRead: 0.03
},
// Claude 3 Opus
'claude-3-opus-20240229': {
input: 15.0,
output: 75.0,
cacheWrite: 18.75,
cacheRead: 1.5
},
// Claude 3 Sonnet
'claude-3-sonnet-20240229': {
input: 3.0,
output: 15.0,
cacheWrite: 3.75,
cacheRead: 0.3
},
// Claude 3 Haiku
'claude-3-haiku-20240307': {
input: 0.25,
output: 1.25,
cacheWrite: 0.3,
cacheRead: 0.03
},
// 默认定价(用于未知模型)
unknown: {
input: 3.0,
output: 15.0,
cacheWrite: 3.75,
cacheRead: 0.3
}
}
class CostCalculator {
/**
* 计算单次请求的费用
* @param {Object} usage - 使用量数据
* @param {number} usage.input_tokens - 输入token数量
* @param {number} usage.output_tokens - 输出token数量
* @param {number} usage.cache_creation_input_tokens - 缓存创建token数量
* @param {number} usage.cache_read_input_tokens - 缓存读取token数量
* @param {string} model - 模型名称
* @returns {Object} 费用详情
*/
static calculateCost(usage, model = 'unknown') {
// 如果 usage 包含详细的 cache_creation 对象,使用 pricingService 来处理
if (usage.cache_creation && typeof usage.cache_creation === 'object') {
return pricingService.calculateCost(usage, model)
}
// 否则使用旧的逻辑(向后兼容)
const inputTokens = usage.input_tokens || 0
const outputTokens = usage.output_tokens || 0
const cacheCreateTokens = usage.cache_creation_input_tokens || 0
const cacheReadTokens = usage.cache_read_input_tokens || 0
// 优先使用动态价格服务
const pricingData = pricingService.getModelPricing(model)
let pricing
let usingDynamicPricing = false
if (pricingData) {
// 转换动态价格格式为内部格式
const inputPrice = (pricingData.input_cost_per_token || 0) * 1000000 // 转换为per 1M tokens
const outputPrice = (pricingData.output_cost_per_token || 0) * 1000000
const cacheReadPrice = (pricingData.cache_read_input_token_cost || 0) * 1000000
// OpenAI 模型的特殊处理:
// - 如果没有 cache_creation_input_token_cost缓存创建按普通 input 价格计费
// - Claude 模型有专门的 cache_creation_input_token_cost
let cacheWritePrice = (pricingData.cache_creation_input_token_cost || 0) * 1000000
// 检测是否为 OpenAI 模型(通过模型名或 litellm_provider
const isOpenAIModel =
model.includes('gpt') || model.includes('o1') || pricingData.litellm_provider === 'openai'
if (isOpenAIModel && !pricingData.cache_creation_input_token_cost && cacheCreateTokens > 0) {
// OpenAI 模型:缓存创建按普通 input 价格计费
cacheWritePrice = inputPrice
}
pricing = {
input: inputPrice,
output: outputPrice,
cacheWrite: cacheWritePrice,
cacheRead: cacheReadPrice
}
usingDynamicPricing = true
} else {
// 回退到静态价格
pricing = MODEL_PRICING[model] || MODEL_PRICING['unknown']
}
// 计算各类型token的费用 (USD)
const inputCost = (inputTokens / 1000000) * pricing.input
const outputCost = (outputTokens / 1000000) * pricing.output
const cacheWriteCost = (cacheCreateTokens / 1000000) * pricing.cacheWrite
const cacheReadCost = (cacheReadTokens / 1000000) * pricing.cacheRead
const totalCost = inputCost + outputCost + cacheWriteCost + cacheReadCost
return {
model,
pricing,
usingDynamicPricing,
usage: {
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
totalTokens: inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
},
costs: {
input: inputCost,
output: outputCost,
cacheWrite: cacheWriteCost,
cacheRead: cacheReadCost,
total: totalCost
},
// 格式化的费用字符串
formatted: {
input: this.formatCost(inputCost),
output: this.formatCost(outputCost),
cacheWrite: this.formatCost(cacheWriteCost),
cacheRead: this.formatCost(cacheReadCost),
total: this.formatCost(totalCost)
},
// 添加调试信息
debug: {
isOpenAIModel: model.includes('gpt') || model.includes('o1'),
hasCacheCreatePrice: !!pricingData?.cache_creation_input_token_cost,
cacheCreateTokens,
cacheWritePriceUsed: pricing.cacheWrite
}
}
}
/**
* 计算聚合使用量的费用
* @param {Object} aggregatedUsage - 聚合使用量数据
* @param {string} model - 模型名称
* @returns {Object} 费用详情
*/
static calculateAggregatedCost(aggregatedUsage, model = 'unknown') {
const usage = {
input_tokens: aggregatedUsage.inputTokens || aggregatedUsage.totalInputTokens || 0,
output_tokens: aggregatedUsage.outputTokens || aggregatedUsage.totalOutputTokens || 0,
cache_creation_input_tokens:
aggregatedUsage.cacheCreateTokens || aggregatedUsage.totalCacheCreateTokens || 0,
cache_read_input_tokens:
aggregatedUsage.cacheReadTokens || aggregatedUsage.totalCacheReadTokens || 0
}
return this.calculateCost(usage, model)
}
/**
* 获取模型定价信息
* @param {string} model - 模型名称
* @returns {Object} 定价信息
*/
static getModelPricing(model = 'unknown') {
return MODEL_PRICING[model] || MODEL_PRICING['unknown']
}
/**
* 获取所有支持的模型和定价
* @returns {Object} 所有模型定价
*/
static getAllModelPricing() {
return { ...MODEL_PRICING }
}
/**
* 验证模型是否支持
* @param {string} model - 模型名称
* @returns {boolean} 是否支持
*/
static isModelSupported(model) {
return !!MODEL_PRICING[model]
}
/**
* 格式化费用显示
* @param {number} cost - 费用金额
* @param {number} decimals - 小数位数
* @returns {string} 格式化的费用字符串
*/
static formatCost(cost, decimals = 6) {
if (cost >= 1) {
return `$${cost.toFixed(2)}`
} else if (cost >= 0.001) {
return `$${cost.toFixed(4)}`
} else {
return `$${cost.toFixed(decimals)}`
}
}
/**
* 计算费用节省(使用缓存的节省)
* @param {Object} usage - 使用量数据
* @param {string} model - 模型名称
* @returns {Object} 节省信息
*/
static calculateCacheSavings(usage, model = 'unknown') {
const pricing = this.getModelPricing(model)
const cacheReadTokens = usage.cache_read_input_tokens || 0
// 如果这些token不使用缓存需要按正常input价格计费
const normalCost = (cacheReadTokens / 1000000) * pricing.input
const cacheCost = (cacheReadTokens / 1000000) * pricing.cacheRead
const savings = normalCost - cacheCost
const savingsPercentage = normalCost > 0 ? (savings / normalCost) * 100 : 0
return {
normalCost,
cacheCost,
savings,
savingsPercentage,
formatted: {
normalCost: this.formatCost(normalCost),
cacheCost: this.formatCost(cacheCost),
savings: this.formatCost(savings),
savingsPercentage: `${savingsPercentage.toFixed(1)}%`
}
}
}
}
module.exports = CostCalculator