mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
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>
This commit is contained in:
@@ -1,103 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenAI Token 刷新功能测试脚本
|
|
||||||
* 用于测试 openaiAccountService 的 token 刷新功能
|
|
||||||
*/
|
|
||||||
|
|
||||||
const openaiAccountService = require('../src/services/openaiAccountService')
|
|
||||||
const logger = require('../src/utils/logger')
|
|
||||||
|
|
||||||
// 测试配置(可以通过环境变量或命令行参数传入)
|
|
||||||
const TEST_REFRESH_TOKEN = process.env.OPENAI_REFRESH_TOKEN || process.argv[2]
|
|
||||||
|
|
||||||
async function testRefreshToken() {
|
|
||||||
if (!TEST_REFRESH_TOKEN) {
|
|
||||||
console.error('❌ 请提供 refresh token 作为参数或设置环境变量 OPENAI_REFRESH_TOKEN')
|
|
||||||
console.log('使用方法:')
|
|
||||||
console.log(' node scripts/test-openai-refresh.js <refresh_token>')
|
|
||||||
console.log(' 或')
|
|
||||||
console.log(' OPENAI_REFRESH_TOKEN=<token> node scripts/test-openai-refresh.js')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🔄 开始测试 OpenAI token 刷新功能...\n')
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 测试不带代理的刷新
|
|
||||||
console.log('1️⃣ 测试直接刷新(无代理)...')
|
|
||||||
const result = await openaiAccountService.refreshAccessToken(TEST_REFRESH_TOKEN)
|
|
||||||
|
|
||||||
console.log('✅ 刷新成功!')
|
|
||||||
console.log(' Access Token:', result.access_token ? result.access_token.substring(0, 30) + '...' : 'N/A')
|
|
||||||
console.log(' ID Token:', result.id_token ? result.id_token.substring(0, 30) + '...' : 'N/A')
|
|
||||||
console.log(' Refresh Token:', result.refresh_token ? result.refresh_token.substring(0, 30) + '...' : 'N/A')
|
|
||||||
console.log(' 有效期:', result.expires_in, '秒')
|
|
||||||
console.log(' 过期时间:', new Date(result.expiry_date).toLocaleString())
|
|
||||||
|
|
||||||
// 如果返回了新的 refresh token
|
|
||||||
if (result.refresh_token && result.refresh_token !== TEST_REFRESH_TOKEN) {
|
|
||||||
console.log('\n⚠️ 注意:收到了新的 refresh token,请保存以供后续使用')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试带代理的刷新(如果配置了代理)
|
|
||||||
if (process.env.PROXY_HOST && process.env.PROXY_PORT) {
|
|
||||||
console.log('\n2️⃣ 测试通过代理刷新...')
|
|
||||||
const proxy = {
|
|
||||||
type: process.env.PROXY_TYPE || 'http',
|
|
||||||
host: process.env.PROXY_HOST,
|
|
||||||
port: parseInt(process.env.PROXY_PORT),
|
|
||||||
username: process.env.PROXY_USERNAME,
|
|
||||||
password: process.env.PROXY_PASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' 代理配置:', `${proxy.type}://${proxy.host}:${proxy.port}`)
|
|
||||||
|
|
||||||
const proxyResult = await openaiAccountService.refreshAccessToken(
|
|
||||||
result.refresh_token || TEST_REFRESH_TOKEN,
|
|
||||||
proxy
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('✅ 通过代理刷新成功!')
|
|
||||||
console.log(' Access Token:', proxyResult.access_token ? proxyResult.access_token.substring(0, 30) + '...' : 'N/A')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试完整的账户刷新流程(如果提供了账户ID)
|
|
||||||
if (process.env.OPENAI_ACCOUNT_ID) {
|
|
||||||
console.log('\n3️⃣ 测试账户刷新流程...')
|
|
||||||
console.log(' 账户ID:', process.env.OPENAI_ACCOUNT_ID)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const account = await openaiAccountService.getAccount(process.env.OPENAI_ACCOUNT_ID)
|
|
||||||
if (account) {
|
|
||||||
console.log(' 账户名称:', account.name)
|
|
||||||
console.log(' 当前过期时间:', account.expiresAt)
|
|
||||||
|
|
||||||
const refreshResult = await openaiAccountService.refreshAccountToken(process.env.OPENAI_ACCOUNT_ID)
|
|
||||||
console.log('✅ 账户 token 刷新成功!')
|
|
||||||
console.log(' 新的过期时间:', new Date(refreshResult.expiry_date).toLocaleString())
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('⚠️ 账户刷新测试失败:', error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n✅ 所有测试完成!')
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('\n❌ 测试失败:', error.message)
|
|
||||||
if (error.response) {
|
|
||||||
console.error('响应状态:', error.response.status)
|
|
||||||
console.error('响应数据:', error.response.data)
|
|
||||||
}
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 运行测试
|
|
||||||
testRefreshToken().then(() => {
|
|
||||||
process.exit(0)
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error('Unexpected error:', error)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
@@ -96,22 +96,42 @@ async function handleMessagesRequest(req, res) {
|
|||||||
) {
|
) {
|
||||||
const inputTokens = usageData.input_tokens || 0
|
const inputTokens = usageData.input_tokens || 0
|
||||||
const outputTokens = usageData.output_tokens || 0
|
const outputTokens = usageData.output_tokens || 0
|
||||||
const cacheCreateTokens = usageData.cache_creation_input_tokens || 0
|
// 兼容处理:如果有详细的 cache_creation 对象,使用它;否则使用总的 cache_creation_input_tokens
|
||||||
|
let cacheCreateTokens = usageData.cache_creation_input_tokens || 0
|
||||||
|
let ephemeral5mTokens = 0
|
||||||
|
let ephemeral1hTokens = 0
|
||||||
|
|
||||||
|
if (usageData.cache_creation && typeof usageData.cache_creation === 'object') {
|
||||||
|
ephemeral5mTokens = usageData.cache_creation.ephemeral_5m_input_tokens || 0
|
||||||
|
ephemeral1hTokens = usageData.cache_creation.ephemeral_1h_input_tokens || 0
|
||||||
|
// 总的缓存创建 tokens 是两者之和
|
||||||
|
cacheCreateTokens = ephemeral5mTokens + ephemeral1hTokens
|
||||||
|
}
|
||||||
|
|
||||||
const cacheReadTokens = usageData.cache_read_input_tokens || 0
|
const cacheReadTokens = usageData.cache_read_input_tokens || 0
|
||||||
const model = usageData.model || 'unknown'
|
const model = usageData.model || 'unknown'
|
||||||
|
|
||||||
// 记录真实的token使用量(包含模型信息和所有4种token以及账户ID)
|
// 记录真实的token使用量(包含模型信息和所有4种token以及账户ID)
|
||||||
const { accountId: usageAccountId } = usageData
|
const { accountId: usageAccountId } = usageData
|
||||||
|
|
||||||
|
// 构建 usage 对象以传递给 recordUsage
|
||||||
|
const usageObject = {
|
||||||
|
input_tokens: inputTokens,
|
||||||
|
output_tokens: outputTokens,
|
||||||
|
cache_creation_input_tokens: cacheCreateTokens,
|
||||||
|
cache_read_input_tokens: cacheReadTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有详细的缓存创建数据,添加到 usage 对象中
|
||||||
|
if (ephemeral5mTokens > 0 || ephemeral1hTokens > 0) {
|
||||||
|
usageObject.cache_creation = {
|
||||||
|
ephemeral_5m_input_tokens: ephemeral5mTokens,
|
||||||
|
ephemeral_1h_input_tokens: ephemeral1hTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
apiKeyService
|
apiKeyService
|
||||||
.recordUsage(
|
.recordUsageWithDetails(req.apiKey.id, usageObject, model, usageAccountId)
|
||||||
req.apiKey.id,
|
|
||||||
inputTokens,
|
|
||||||
outputTokens,
|
|
||||||
cacheCreateTokens,
|
|
||||||
cacheReadTokens,
|
|
||||||
model,
|
|
||||||
usageAccountId
|
|
||||||
)
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error('❌ Failed to record stream usage:', error)
|
logger.error('❌ Failed to record stream usage:', error)
|
||||||
})
|
})
|
||||||
@@ -161,22 +181,42 @@ async function handleMessagesRequest(req, res) {
|
|||||||
) {
|
) {
|
||||||
const inputTokens = usageData.input_tokens || 0
|
const inputTokens = usageData.input_tokens || 0
|
||||||
const outputTokens = usageData.output_tokens || 0
|
const outputTokens = usageData.output_tokens || 0
|
||||||
const cacheCreateTokens = usageData.cache_creation_input_tokens || 0
|
// 兼容处理:如果有详细的 cache_creation 对象,使用它;否则使用总的 cache_creation_input_tokens
|
||||||
|
let cacheCreateTokens = usageData.cache_creation_input_tokens || 0
|
||||||
|
let ephemeral5mTokens = 0
|
||||||
|
let ephemeral1hTokens = 0
|
||||||
|
|
||||||
|
if (usageData.cache_creation && typeof usageData.cache_creation === 'object') {
|
||||||
|
ephemeral5mTokens = usageData.cache_creation.ephemeral_5m_input_tokens || 0
|
||||||
|
ephemeral1hTokens = usageData.cache_creation.ephemeral_1h_input_tokens || 0
|
||||||
|
// 总的缓存创建 tokens 是两者之和
|
||||||
|
cacheCreateTokens = ephemeral5mTokens + ephemeral1hTokens
|
||||||
|
}
|
||||||
|
|
||||||
const cacheReadTokens = usageData.cache_read_input_tokens || 0
|
const cacheReadTokens = usageData.cache_read_input_tokens || 0
|
||||||
const model = usageData.model || 'unknown'
|
const model = usageData.model || 'unknown'
|
||||||
|
|
||||||
// 记录真实的token使用量(包含模型信息和所有4种token以及账户ID)
|
// 记录真实的token使用量(包含模型信息和所有4种token以及账户ID)
|
||||||
const usageAccountId = usageData.accountId
|
const usageAccountId = usageData.accountId
|
||||||
|
|
||||||
|
// 构建 usage 对象以传递给 recordUsage
|
||||||
|
const usageObject = {
|
||||||
|
input_tokens: inputTokens,
|
||||||
|
output_tokens: outputTokens,
|
||||||
|
cache_creation_input_tokens: cacheCreateTokens,
|
||||||
|
cache_read_input_tokens: cacheReadTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有详细的缓存创建数据,添加到 usage 对象中
|
||||||
|
if (ephemeral5mTokens > 0 || ephemeral1hTokens > 0) {
|
||||||
|
usageObject.cache_creation = {
|
||||||
|
ephemeral_5m_input_tokens: ephemeral5mTokens,
|
||||||
|
ephemeral_1h_input_tokens: ephemeral1hTokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
apiKeyService
|
apiKeyService
|
||||||
.recordUsage(
|
.recordUsageWithDetails(req.apiKey.id, usageObject, model, usageAccountId)
|
||||||
req.apiKey.id,
|
|
||||||
inputTokens,
|
|
||||||
outputTokens,
|
|
||||||
cacheCreateTokens,
|
|
||||||
cacheReadTokens,
|
|
||||||
model,
|
|
||||||
usageAccountId
|
|
||||||
)
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error('❌ Failed to record stream usage:', error)
|
logger.error('❌ Failed to record stream usage:', error)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -455,6 +455,104 @@ class ApiKeyService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 📊 记录使用情况(新版本,支持详细的缓存类型)
|
||||||
|
async recordUsageWithDetails(keyId, usageObject, model = 'unknown', accountId = null) {
|
||||||
|
try {
|
||||||
|
// 提取 token 数量
|
||||||
|
const inputTokens = usageObject.input_tokens || 0
|
||||||
|
const outputTokens = usageObject.output_tokens || 0
|
||||||
|
const cacheCreateTokens = usageObject.cache_creation_input_tokens || 0
|
||||||
|
const cacheReadTokens = usageObject.cache_read_input_tokens || 0
|
||||||
|
|
||||||
|
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
|
||||||
|
|
||||||
|
// 计算费用(支持详细的缓存类型)
|
||||||
|
const pricingService = require('./pricingService')
|
||||||
|
const costInfo = pricingService.calculateCost(usageObject, model)
|
||||||
|
|
||||||
|
// 记录API Key级别的使用统计
|
||||||
|
await redis.incrementTokenUsage(
|
||||||
|
keyId,
|
||||||
|
totalTokens,
|
||||||
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheCreateTokens,
|
||||||
|
cacheReadTokens,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
|
||||||
|
// 记录费用统计
|
||||||
|
if (costInfo.totalCost > 0) {
|
||||||
|
await redis.incrementDailyCost(keyId, costInfo.totalCost)
|
||||||
|
logger.database(
|
||||||
|
`💰 Recorded cost for ${keyId}: $${costInfo.totalCost.toFixed(6)}, model: ${model}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// 记录详细的缓存费用(如果有)
|
||||||
|
if (costInfo.ephemeral5mCost > 0 || costInfo.ephemeral1hCost > 0) {
|
||||||
|
logger.database(
|
||||||
|
`💰 Cache costs - 5m: $${costInfo.ephemeral5mCost.toFixed(6)}, 1h: $${costInfo.ephemeral1hCost.toFixed(6)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug(`💰 No cost recorded for ${keyId} - zero cost for model: ${model}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取API Key数据以确定关联的账户
|
||||||
|
const keyData = await redis.getApiKey(keyId)
|
||||||
|
if (keyData && Object.keys(keyData).length > 0) {
|
||||||
|
// 更新最后使用时间
|
||||||
|
keyData.lastUsedAt = new Date().toISOString()
|
||||||
|
await redis.setApiKey(keyId, keyData)
|
||||||
|
|
||||||
|
// 记录账户级别的使用统计(只统计实际处理请求的账户)
|
||||||
|
if (accountId) {
|
||||||
|
await redis.incrementAccountUsage(
|
||||||
|
accountId,
|
||||||
|
totalTokens,
|
||||||
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheCreateTokens,
|
||||||
|
cacheReadTokens,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
logger.database(
|
||||||
|
`📊 Recorded account usage: ${accountId} - ${totalTokens} tokens (API Key: ${keyId})`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
'⚠️ No accountId provided for usage recording, skipping account-level statistics'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logParts = [`Model: ${model}`, `Input: ${inputTokens}`, `Output: ${outputTokens}`]
|
||||||
|
if (cacheCreateTokens > 0) {
|
||||||
|
logParts.push(`Cache Create: ${cacheCreateTokens}`)
|
||||||
|
|
||||||
|
// 如果有详细的缓存创建数据,也记录它们
|
||||||
|
if (usageObject.cache_creation) {
|
||||||
|
const { ephemeral_5m_input_tokens, ephemeral_1h_input_tokens } =
|
||||||
|
usageObject.cache_creation
|
||||||
|
if (ephemeral_5m_input_tokens > 0) {
|
||||||
|
logParts.push(`5m: ${ephemeral_5m_input_tokens}`)
|
||||||
|
}
|
||||||
|
if (ephemeral_1h_input_tokens > 0) {
|
||||||
|
logParts.push(`1h: ${ephemeral_1h_input_tokens}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cacheReadTokens > 0) {
|
||||||
|
logParts.push(`Cache Read: ${cacheReadTokens}`)
|
||||||
|
}
|
||||||
|
logParts.push(`Total: ${totalTokens} tokens`)
|
||||||
|
|
||||||
|
logger.database(`📊 Recorded usage: ${keyId} - ${logParts.join(', ')}`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Failed to record usage:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🔐 生成密钥
|
// 🔐 生成密钥
|
||||||
_generateSecretKey() {
|
_generateSecretKey() {
|
||||||
return crypto.randomBytes(32).toString('hex')
|
return crypto.randomBytes(32).toString('hex')
|
||||||
|
|||||||
@@ -451,6 +451,23 @@ class ClaudeConsoleRelayService {
|
|||||||
collectedUsageData.cache_read_input_tokens =
|
collectedUsageData.cache_read_input_tokens =
|
||||||
data.message.usage.cache_read_input_tokens || 0
|
data.message.usage.cache_read_input_tokens || 0
|
||||||
collectedUsageData.model = data.message.model
|
collectedUsageData.model = data.message.model
|
||||||
|
|
||||||
|
// 检查是否有详细的 cache_creation 对象
|
||||||
|
if (
|
||||||
|
data.message.usage.cache_creation &&
|
||||||
|
typeof data.message.usage.cache_creation === 'object'
|
||||||
|
) {
|
||||||
|
collectedUsageData.cache_creation = {
|
||||||
|
ephemeral_5m_input_tokens:
|
||||||
|
data.message.usage.cache_creation.ephemeral_5m_input_tokens || 0,
|
||||||
|
ephemeral_1h_input_tokens:
|
||||||
|
data.message.usage.cache_creation.ephemeral_1h_input_tokens || 0
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
'📊 Collected detailed cache creation data:',
|
||||||
|
JSON.stringify(collectedUsageData.cache_creation)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -939,6 +939,23 @@ class ClaudeRelayService {
|
|||||||
data.message.usage.cache_read_input_tokens || 0
|
data.message.usage.cache_read_input_tokens || 0
|
||||||
collectedUsageData.model = data.message.model
|
collectedUsageData.model = data.message.model
|
||||||
|
|
||||||
|
// 检查是否有详细的 cache_creation 对象
|
||||||
|
if (
|
||||||
|
data.message.usage.cache_creation &&
|
||||||
|
typeof data.message.usage.cache_creation === 'object'
|
||||||
|
) {
|
||||||
|
collectedUsageData.cache_creation = {
|
||||||
|
ephemeral_5m_input_tokens:
|
||||||
|
data.message.usage.cache_creation.ephemeral_5m_input_tokens || 0,
|
||||||
|
ephemeral_1h_input_tokens:
|
||||||
|
data.message.usage.cache_creation.ephemeral_1h_input_tokens || 0
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
'📊 Collected detailed cache creation data:',
|
||||||
|
JSON.stringify(collectedUsageData.cache_creation)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'📊 Collected input/cache data from message_start:',
|
'📊 Collected input/cache data from message_start:',
|
||||||
JSON.stringify(collectedUsageData)
|
JSON.stringify(collectedUsageData)
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ async function refreshAccessToken(refreshToken, proxy = null) {
|
|||||||
try {
|
try {
|
||||||
// Codex CLI 的官方 CLIENT_ID
|
// Codex CLI 的官方 CLIENT_ID
|
||||||
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'
|
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'
|
||||||
|
|
||||||
// 准备请求数据
|
// 准备请求数据
|
||||||
const requestData = new URLSearchParams({
|
const requestData = new URLSearchParams({
|
||||||
grant_type: 'refresh_token',
|
grant_type: 'refresh_token',
|
||||||
@@ -96,15 +96,13 @@ async function refreshAccessToken(refreshToken, proxy = null) {
|
|||||||
// 配置代理(如果有)
|
// 配置代理(如果有)
|
||||||
if (proxy && proxy.host && proxy.port) {
|
if (proxy && proxy.host && proxy.port) {
|
||||||
if (proxy.type === 'socks5') {
|
if (proxy.type === 'socks5') {
|
||||||
const proxyAuth = proxy.username && proxy.password
|
const proxyAuth =
|
||||||
? `${proxy.username}:${proxy.password}@`
|
proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : ''
|
||||||
: ''
|
|
||||||
const socksProxy = `socks5://${proxyAuth}${proxy.host}:${proxy.port}`
|
const socksProxy = `socks5://${proxyAuth}${proxy.host}:${proxy.port}`
|
||||||
requestOptions.httpsAgent = new SocksProxyAgent(socksProxy)
|
requestOptions.httpsAgent = new SocksProxyAgent(socksProxy)
|
||||||
} else if (proxy.type === 'http' || proxy.type === 'https') {
|
} else if (proxy.type === 'http' || proxy.type === 'https') {
|
||||||
const proxyAuth = proxy.username && proxy.password
|
const proxyAuth =
|
||||||
? `${proxy.username}:${proxy.password}@`
|
proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : ''
|
||||||
: ''
|
|
||||||
const httpProxy = `http://${proxyAuth}${proxy.host}:${proxy.port}`
|
const httpProxy = `http://${proxyAuth}${proxy.host}:${proxy.port}`
|
||||||
requestOptions.httpsAgent = new HttpsProxyAgent(httpProxy)
|
requestOptions.httpsAgent = new HttpsProxyAgent(httpProxy)
|
||||||
}
|
}
|
||||||
@@ -115,16 +113,16 @@ async function refreshAccessToken(refreshToken, proxy = null) {
|
|||||||
|
|
||||||
if (response.status === 200 && response.data) {
|
if (response.status === 200 && response.data) {
|
||||||
const result = response.data
|
const result = response.data
|
||||||
|
|
||||||
logger.info('✅ Successfully refreshed OpenAI token')
|
logger.info('✅ Successfully refreshed OpenAI token')
|
||||||
|
|
||||||
// 返回新的 token 信息
|
// 返回新的 token 信息
|
||||||
return {
|
return {
|
||||||
access_token: result.access_token,
|
access_token: result.access_token,
|
||||||
id_token: result.id_token,
|
id_token: result.id_token,
|
||||||
refresh_token: result.refresh_token || refreshToken, // 如果没有返回新的,保留原来的
|
refresh_token: result.refresh_token || refreshToken, // 如果没有返回新的,保留原来的
|
||||||
expires_in: result.expires_in || 3600,
|
expires_in: result.expires_in || 3600,
|
||||||
expiry_date: Date.now() + ((result.expires_in || 3600) * 1000) // 计算过期时间
|
expiry_date: Date.now() + (result.expires_in || 3600) * 1000 // 计算过期时间
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Failed to refresh token: ${response.status} ${response.statusText}`)
|
throw new Error(`Failed to refresh token: ${response.status} ${response.statusText}`)
|
||||||
@@ -137,7 +135,9 @@ async function refreshAccessToken(refreshToken, proxy = null) {
|
|||||||
data: error.response.data,
|
data: error.response.data,
|
||||||
headers: error.response.headers
|
headers: error.response.headers
|
||||||
})
|
})
|
||||||
throw new Error(`Token refresh failed: ${error.response.status} - ${JSON.stringify(error.response.data)}`)
|
throw new Error(
|
||||||
|
`Token refresh failed: ${error.response.status} - ${JSON.stringify(error.response.data)}`
|
||||||
|
)
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
// 请求已发出但没有收到响应
|
// 请求已发出但没有收到响应
|
||||||
logger.error('OpenAI token refresh no response:', error.message)
|
logger.error('OpenAI token refresh no response:', error.message)
|
||||||
|
|||||||
@@ -20,6 +20,41 @@ class PricingService {
|
|||||||
this.updateInterval = 24 * 60 * 60 * 1000 // 24小时
|
this.updateInterval = 24 * 60 * 60 * 1000 // 24小时
|
||||||
this.fileWatcher = null // 文件监听器
|
this.fileWatcher = null // 文件监听器
|
||||||
this.reloadDebounceTimer = null // 防抖定时器
|
this.reloadDebounceTimer = null // 防抖定时器
|
||||||
|
|
||||||
|
// 硬编码的 1 小时缓存价格(美元/百万 token)
|
||||||
|
// ephemeral_5m 的价格使用 model_pricing.json 中的 cache_creation_input_token_cost
|
||||||
|
// ephemeral_1h 的价格需要硬编码
|
||||||
|
this.ephemeral1hPricing = {
|
||||||
|
// Opus 系列: $30/MTok
|
||||||
|
'claude-opus-4-1': 0.00003,
|
||||||
|
'claude-opus-4-1-20250805': 0.00003,
|
||||||
|
'claude-opus-4': 0.00003,
|
||||||
|
'claude-opus-4-20250514': 0.00003,
|
||||||
|
'claude-3-opus': 0.00003,
|
||||||
|
'claude-3-opus-latest': 0.00003,
|
||||||
|
'claude-3-opus-20240229': 0.00003,
|
||||||
|
|
||||||
|
// Sonnet 系列: $6/MTok
|
||||||
|
'claude-3-5-sonnet': 0.000006,
|
||||||
|
'claude-3-5-sonnet-latest': 0.000006,
|
||||||
|
'claude-3-5-sonnet-20241022': 0.000006,
|
||||||
|
'claude-3-5-sonnet-20240620': 0.000006,
|
||||||
|
'claude-3-sonnet': 0.000006,
|
||||||
|
'claude-3-sonnet-20240307': 0.000006,
|
||||||
|
'claude-sonnet-3': 0.000006,
|
||||||
|
'claude-sonnet-3-5': 0.000006,
|
||||||
|
'claude-sonnet-3-7': 0.000006,
|
||||||
|
'claude-sonnet-4': 0.000006,
|
||||||
|
|
||||||
|
// Haiku 系列: $1.6/MTok
|
||||||
|
'claude-3-5-haiku': 0.0000016,
|
||||||
|
'claude-3-5-haiku-latest': 0.0000016,
|
||||||
|
'claude-3-5-haiku-20241022': 0.0000016,
|
||||||
|
'claude-3-haiku': 0.0000016,
|
||||||
|
'claude-3-haiku-20240307': 0.0000016,
|
||||||
|
'claude-haiku-3': 0.0000016,
|
||||||
|
'claude-haiku-3-5': 0.0000016
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化价格服务
|
// 初始化价格服务
|
||||||
@@ -258,6 +293,40 @@ class PricingService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取 1 小时缓存价格
|
||||||
|
getEphemeral1hPricing(modelName) {
|
||||||
|
if (!modelName) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试直接匹配
|
||||||
|
if (this.ephemeral1hPricing[modelName]) {
|
||||||
|
return this.ephemeral1hPricing[modelName]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理各种模型名称变体
|
||||||
|
const modelLower = modelName.toLowerCase()
|
||||||
|
|
||||||
|
// 检查是否是 Opus 系列
|
||||||
|
if (modelLower.includes('opus')) {
|
||||||
|
return 0.00003 // $30/MTok
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是 Sonnet 系列
|
||||||
|
if (modelLower.includes('sonnet')) {
|
||||||
|
return 0.000006 // $6/MTok
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是 Haiku 系列
|
||||||
|
if (modelLower.includes('haiku')) {
|
||||||
|
return 0.0000016 // $1.6/MTok
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认返回 0(未知模型)
|
||||||
|
logger.debug(`💰 No 1h cache pricing found for model: ${modelName}`)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// 计算使用费用
|
// 计算使用费用
|
||||||
calculateCost(usage, modelName) {
|
calculateCost(usage, modelName) {
|
||||||
const pricing = this.getModelPricing(modelName)
|
const pricing = this.getModelPricing(modelName)
|
||||||
@@ -268,6 +337,8 @@ class PricingService {
|
|||||||
outputCost: 0,
|
outputCost: 0,
|
||||||
cacheCreateCost: 0,
|
cacheCreateCost: 0,
|
||||||
cacheReadCost: 0,
|
cacheReadCost: 0,
|
||||||
|
ephemeral5mCost: 0,
|
||||||
|
ephemeral1hCost: 0,
|
||||||
totalCost: 0,
|
totalCost: 0,
|
||||||
hasPricing: false
|
hasPricing: false
|
||||||
}
|
}
|
||||||
@@ -275,23 +346,52 @@ class PricingService {
|
|||||||
|
|
||||||
const inputCost = (usage.input_tokens || 0) * (pricing.input_cost_per_token || 0)
|
const inputCost = (usage.input_tokens || 0) * (pricing.input_cost_per_token || 0)
|
||||||
const outputCost = (usage.output_tokens || 0) * (pricing.output_cost_per_token || 0)
|
const outputCost = (usage.output_tokens || 0) * (pricing.output_cost_per_token || 0)
|
||||||
const cacheCreateCost =
|
|
||||||
(usage.cache_creation_input_tokens || 0) * (pricing.cache_creation_input_token_cost || 0)
|
|
||||||
const cacheReadCost =
|
const cacheReadCost =
|
||||||
(usage.cache_read_input_tokens || 0) * (pricing.cache_read_input_token_cost || 0)
|
(usage.cache_read_input_tokens || 0) * (pricing.cache_read_input_token_cost || 0)
|
||||||
|
|
||||||
|
// 处理缓存创建费用:
|
||||||
|
// 1. 如果有详细的 cache_creation 对象,使用它
|
||||||
|
// 2. 否则使用总的 cache_creation_input_tokens(向后兼容)
|
||||||
|
let ephemeral5mCost = 0
|
||||||
|
let ephemeral1hCost = 0
|
||||||
|
let cacheCreateCost = 0
|
||||||
|
|
||||||
|
if (usage.cache_creation && typeof usage.cache_creation === 'object') {
|
||||||
|
// 有详细的缓存创建数据
|
||||||
|
const ephemeral5mTokens = usage.cache_creation.ephemeral_5m_input_tokens || 0
|
||||||
|
const ephemeral1hTokens = usage.cache_creation.ephemeral_1h_input_tokens || 0
|
||||||
|
|
||||||
|
// 5分钟缓存使用标准的 cache_creation_input_token_cost
|
||||||
|
ephemeral5mCost = ephemeral5mTokens * (pricing.cache_creation_input_token_cost || 0)
|
||||||
|
|
||||||
|
// 1小时缓存使用硬编码的价格
|
||||||
|
const ephemeral1hPrice = this.getEphemeral1hPricing(modelName)
|
||||||
|
ephemeral1hCost = ephemeral1hTokens * ephemeral1hPrice
|
||||||
|
|
||||||
|
// 总的缓存创建费用
|
||||||
|
cacheCreateCost = ephemeral5mCost + ephemeral1hCost
|
||||||
|
} else if (usage.cache_creation_input_tokens) {
|
||||||
|
// 旧格式,所有缓存创建 tokens 都按 5 分钟价格计算(向后兼容)
|
||||||
|
cacheCreateCost =
|
||||||
|
(usage.cache_creation_input_tokens || 0) * (pricing.cache_creation_input_token_cost || 0)
|
||||||
|
ephemeral5mCost = cacheCreateCost
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputCost,
|
inputCost,
|
||||||
outputCost,
|
outputCost,
|
||||||
cacheCreateCost,
|
cacheCreateCost,
|
||||||
cacheReadCost,
|
cacheReadCost,
|
||||||
|
ephemeral5mCost,
|
||||||
|
ephemeral1hCost,
|
||||||
totalCost: inputCost + outputCost + cacheCreateCost + cacheReadCost,
|
totalCost: inputCost + outputCost + cacheCreateCost + cacheReadCost,
|
||||||
hasPricing: true,
|
hasPricing: true,
|
||||||
pricing: {
|
pricing: {
|
||||||
input: pricing.input_cost_per_token || 0,
|
input: pricing.input_cost_per_token || 0,
|
||||||
output: pricing.output_cost_per_token || 0,
|
output: pricing.output_cost_per_token || 0,
|
||||||
cacheCreate: pricing.cache_creation_input_token_cost || 0,
|
cacheCreate: pricing.cache_creation_input_token_cost || 0,
|
||||||
cacheRead: pricing.cache_read_input_token_cost || 0
|
cacheRead: pricing.cache_read_input_token_cost || 0,
|
||||||
|
ephemeral1h: this.getEphemeral1hPricing(modelName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ class CostCalculator {
|
|||||||
* @returns {Object} 费用详情
|
* @returns {Object} 费用详情
|
||||||
*/
|
*/
|
||||||
static calculateCost(usage, model = 'unknown') {
|
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 inputTokens = usage.input_tokens || 0
|
||||||
const outputTokens = usage.output_tokens || 0
|
const outputTokens = usage.output_tokens || 0
|
||||||
const cacheCreateTokens = usage.cache_creation_input_tokens || 0
|
const cacheCreateTokens = usage.cache_creation_input_tokens || 0
|
||||||
|
|||||||
Reference in New Issue
Block a user