mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: 修复openai输入token计算问题
This commit is contained in:
@@ -1,157 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const path = require('path')
|
|
||||||
const Module = require('module')
|
|
||||||
|
|
||||||
const originalResolveFilename = Module._resolveFilename
|
|
||||||
Module._resolveFilename = function resolveConfig(request, parent, isMain, options) {
|
|
||||||
if (request.endsWith('/config/config')) {
|
|
||||||
return path.resolve(__dirname, '../config/config.example.js')
|
|
||||||
}
|
|
||||||
return originalResolveFilename.call(this, request, parent, isMain, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
const redis = require('../src/models/redis')
|
|
||||||
const apiKeyService = require('../src/services/apiKeyService')
|
|
||||||
const { authenticateApiKey } = require('../src/middleware/auth')
|
|
||||||
|
|
||||||
Module._resolveFilename = originalResolveFilename
|
|
||||||
|
|
||||||
function createMockReq(apiKey) {
|
|
||||||
return {
|
|
||||||
headers: {
|
|
||||||
'x-api-key': apiKey,
|
|
||||||
'user-agent': 'total-cost-limit-test'
|
|
||||||
},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
ip: '127.0.0.1',
|
|
||||||
connection: {},
|
|
||||||
originalUrl: '/test-total-cost-limit',
|
|
||||||
once: () => {},
|
|
||||||
on: () => {},
|
|
||||||
get(header) {
|
|
||||||
return this.headers[header.toLowerCase()] || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMockRes() {
|
|
||||||
const state = {
|
|
||||||
status: 200,
|
|
||||||
body: null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
once: () => {},
|
|
||||||
on: () => {},
|
|
||||||
status(code) {
|
|
||||||
state.status = code
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
json(payload) {
|
|
||||||
state.body = payload
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
getState() {
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runAuth(apiKey) {
|
|
||||||
const req = createMockReq(apiKey)
|
|
||||||
const res = createMockRes()
|
|
||||||
let nextCalled = false
|
|
||||||
|
|
||||||
await authenticateApiKey(req, res, () => {
|
|
||||||
nextCalled = true
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = res.getState()
|
|
||||||
if (nextCalled && result.status === 200) {
|
|
||||||
return { status: 200, body: null }
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cleanupKey(keyId) {
|
|
||||||
const client = redis.getClient()
|
|
||||||
if (!client) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await redis.deleteApiKey(keyId)
|
|
||||||
const usageKeys = await client.keys(`usage:*:${keyId}*`)
|
|
||||||
if (usageKeys.length > 0) {
|
|
||||||
await client.del(...usageKeys)
|
|
||||||
}
|
|
||||||
const costKeys = await client.keys(`usage:cost:*:${keyId}*`)
|
|
||||||
if (costKeys.length > 0) {
|
|
||||||
await client.del(...costKeys)
|
|
||||||
}
|
|
||||||
await client.del(`usage:${keyId}`)
|
|
||||||
await client.del(`usage:records:${keyId}`)
|
|
||||||
await client.del(`usage:cost:total:${keyId}`)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to cleanup test key ${keyId}:`, error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await redis.connect()
|
|
||||||
|
|
||||||
const testName = `TotalCostLimitTest-${Date.now()}`
|
|
||||||
const totalCostLimit = 1.0
|
|
||||||
const newKey = await apiKeyService.generateApiKey({
|
|
||||||
name: testName,
|
|
||||||
permissions: 'all',
|
|
||||||
totalCostLimit: totalCostLimit
|
|
||||||
})
|
|
||||||
|
|
||||||
const keyId = newKey.id
|
|
||||||
const { apiKey } = newKey
|
|
||||||
|
|
||||||
console.log(`➕ Created test API key ${keyId} with total cost limit $${totalCostLimit}`)
|
|
||||||
|
|
||||||
let authResult = await runAuth(apiKey)
|
|
||||||
if (authResult.status !== 200) {
|
|
||||||
throw new Error(`Expected success before any usage, got status ${authResult.status}`)
|
|
||||||
}
|
|
||||||
console.log('✅ Authentication succeeds before consuming quota')
|
|
||||||
|
|
||||||
// 增加总费用
|
|
||||||
const client = redis.getClient()
|
|
||||||
await client.set(`usage:cost:total:${keyId}`, '0.6')
|
|
||||||
|
|
||||||
authResult = await runAuth(apiKey)
|
|
||||||
if (authResult.status !== 200) {
|
|
||||||
throw new Error(`Expected success under quota, got status ${authResult.status}`)
|
|
||||||
}
|
|
||||||
console.log('✅ Authentication succeeds while still under quota ($0.60)')
|
|
||||||
|
|
||||||
// 继续增加总费用超过限制
|
|
||||||
await client.set(`usage:cost:total:${keyId}`, '1.1')
|
|
||||||
|
|
||||||
authResult = await runAuth(apiKey)
|
|
||||||
if (authResult.status !== 429) {
|
|
||||||
throw new Error(`Expected 429 after exceeding quota, got status ${authResult.status}`)
|
|
||||||
}
|
|
||||||
console.log('✅ Authentication returns 429 after exceeding total cost limit ($1.10)')
|
|
||||||
|
|
||||||
await cleanupKey(keyId)
|
|
||||||
await redis.disconnect()
|
|
||||||
|
|
||||||
console.log('🎉 Total cost limit test completed successfully')
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(async (error) => {
|
|
||||||
console.error('❌ Total cost limit test failed:', error)
|
|
||||||
try {
|
|
||||||
await redis.disconnect()
|
|
||||||
} catch (_) {
|
|
||||||
// Ignore disconnect errors during cleanup
|
|
||||||
}
|
|
||||||
process.exitCode = 1
|
|
||||||
})
|
|
||||||
@@ -390,23 +390,24 @@ const handleResponses = async (req, res) => {
|
|||||||
|
|
||||||
// 记录使用统计
|
// 记录使用统计
|
||||||
if (usageData) {
|
if (usageData) {
|
||||||
const inputTokens = usageData.input_tokens || usageData.prompt_tokens || 0
|
const totalInputTokens = usageData.input_tokens || usageData.prompt_tokens || 0
|
||||||
const outputTokens = usageData.output_tokens || usageData.completion_tokens || 0
|
const outputTokens = usageData.output_tokens || usageData.completion_tokens || 0
|
||||||
const cacheCreateTokens = usageData.input_tokens_details?.cache_creation_tokens || 0
|
|
||||||
const cacheReadTokens = usageData.input_tokens_details?.cached_tokens || 0
|
const cacheReadTokens = usageData.input_tokens_details?.cached_tokens || 0
|
||||||
|
// 计算实际输入token(总输入减去缓存部分)
|
||||||
|
const actualInputTokens = Math.max(0, totalInputTokens - cacheReadTokens)
|
||||||
|
|
||||||
await apiKeyService.recordUsage(
|
await apiKeyService.recordUsage(
|
||||||
apiKeyData.id,
|
apiKeyData.id,
|
||||||
inputTokens,
|
actualInputTokens, // 传递实际输入(不含缓存)
|
||||||
outputTokens,
|
outputTokens,
|
||||||
cacheCreateTokens,
|
0, // OpenAI没有cache_creation_tokens
|
||||||
cacheReadTokens,
|
cacheReadTokens,
|
||||||
actualModel,
|
actualModel,
|
||||||
accountId
|
accountId
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`📊 Recorded OpenAI non-stream usage - Input: ${inputTokens}, Output: ${outputTokens}, Total: ${usageData.total_tokens || inputTokens + outputTokens}, Model: ${actualModel}`
|
`📊 Recorded OpenAI non-stream usage - Input: ${totalInputTokens}(actual:${actualInputTokens}+cached:${cacheReadTokens}), Output: ${outputTokens}, Total: ${usageData.total_tokens || totalInputTokens + outputTokens}, Model: ${actualModel}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,26 +507,27 @@ const handleResponses = async (req, res) => {
|
|||||||
// 记录使用统计
|
// 记录使用统计
|
||||||
if (!usageReported && usageData) {
|
if (!usageReported && usageData) {
|
||||||
try {
|
try {
|
||||||
const inputTokens = usageData.input_tokens || 0
|
const totalInputTokens = usageData.input_tokens || 0
|
||||||
const outputTokens = usageData.output_tokens || 0
|
const outputTokens = usageData.output_tokens || 0
|
||||||
const cacheCreateTokens = usageData.input_tokens_details?.cache_creation_tokens || 0
|
|
||||||
const cacheReadTokens = usageData.input_tokens_details?.cached_tokens || 0
|
const cacheReadTokens = usageData.input_tokens_details?.cached_tokens || 0
|
||||||
|
// 计算实际输入token(总输入减去缓存部分)
|
||||||
|
const actualInputTokens = Math.max(0, totalInputTokens - cacheReadTokens)
|
||||||
|
|
||||||
// 使用响应中的真实 model,如果没有则使用请求中的 model,最后回退到默认值
|
// 使用响应中的真实 model,如果没有则使用请求中的 model,最后回退到默认值
|
||||||
const modelToRecord = actualModel || requestedModel || 'gpt-4'
|
const modelToRecord = actualModel || requestedModel || 'gpt-4'
|
||||||
|
|
||||||
await apiKeyService.recordUsage(
|
await apiKeyService.recordUsage(
|
||||||
apiKeyData.id,
|
apiKeyData.id,
|
||||||
inputTokens,
|
actualInputTokens, // 传递实际输入(不含缓存)
|
||||||
outputTokens,
|
outputTokens,
|
||||||
cacheCreateTokens,
|
0, // OpenAI没有cache_creation_tokens
|
||||||
cacheReadTokens,
|
cacheReadTokens,
|
||||||
modelToRecord,
|
modelToRecord,
|
||||||
accountId
|
accountId
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`📊 Recorded OpenAI usage - Input: ${inputTokens}, Output: ${outputTokens}, Total: ${usageData.total_tokens || inputTokens + outputTokens}, Model: ${modelToRecord} (actual: ${actualModel}, requested: ${requestedModel})`
|
`📊 Recorded OpenAI usage - Input: ${totalInputTokens}(actual:${actualInputTokens}+cached:${cacheReadTokens}), Output: ${outputTokens}, Total: ${usageData.total_tokens || totalInputTokens + outputTokens}, Model: ${modelToRecord} (actual: ${actualModel}, requested: ${requestedModel})`
|
||||||
)
|
)
|
||||||
usageReported = true
|
usageReported = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -385,28 +385,29 @@ class OpenAIResponsesRelayService {
|
|||||||
if (usageData) {
|
if (usageData) {
|
||||||
try {
|
try {
|
||||||
// OpenAI-Responses 使用 input_tokens/output_tokens,标准 OpenAI 使用 prompt_tokens/completion_tokens
|
// OpenAI-Responses 使用 input_tokens/output_tokens,标准 OpenAI 使用 prompt_tokens/completion_tokens
|
||||||
const inputTokens = usageData.input_tokens || usageData.prompt_tokens || 0
|
const totalInputTokens = usageData.input_tokens || usageData.prompt_tokens || 0
|
||||||
const outputTokens = usageData.output_tokens || usageData.completion_tokens || 0
|
const outputTokens = usageData.output_tokens || usageData.completion_tokens || 0
|
||||||
|
|
||||||
// 提取缓存相关的 tokens(如果存在)
|
// 提取缓存相关的 tokens(如果存在)
|
||||||
const cacheCreateTokens = usageData.input_tokens_details?.cache_creation_tokens || 0
|
|
||||||
const cacheReadTokens = usageData.input_tokens_details?.cached_tokens || 0
|
const cacheReadTokens = usageData.input_tokens_details?.cached_tokens || 0
|
||||||
|
// 计算实际输入token(总输入减去缓存部分)
|
||||||
|
const actualInputTokens = Math.max(0, totalInputTokens - cacheReadTokens)
|
||||||
|
|
||||||
const totalTokens = usageData.total_tokens || inputTokens + outputTokens
|
const totalTokens = usageData.total_tokens || totalInputTokens + outputTokens
|
||||||
const modelToRecord = actualModel || requestedModel || 'gpt-4'
|
const modelToRecord = actualModel || requestedModel || 'gpt-4'
|
||||||
|
|
||||||
await apiKeyService.recordUsage(
|
await apiKeyService.recordUsage(
|
||||||
apiKeyData.id,
|
apiKeyData.id,
|
||||||
inputTokens,
|
actualInputTokens, // 传递实际输入(不含缓存)
|
||||||
outputTokens,
|
outputTokens,
|
||||||
cacheCreateTokens,
|
0, // OpenAI没有cache_creation_tokens
|
||||||
cacheReadTokens,
|
cacheReadTokens,
|
||||||
modelToRecord,
|
modelToRecord,
|
||||||
account.id
|
account.id
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`📊 Recorded usage - Input: ${inputTokens}, Output: ${outputTokens}, CacheRead: ${cacheReadTokens}, CacheCreate: ${cacheCreateTokens}, Total: ${totalTokens}, Model: ${modelToRecord}`
|
`📊 Recorded usage - Input: ${totalInputTokens}(actual:${actualInputTokens}+cached:${cacheReadTokens}), Output: ${outputTokens}, Total: ${totalTokens}, Model: ${modelToRecord}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// 更新账户的 token 使用统计
|
// 更新账户的 token 使用统计
|
||||||
@@ -414,9 +415,18 @@ class OpenAIResponsesRelayService {
|
|||||||
|
|
||||||
// 更新账户使用额度(如果设置了额度限制)
|
// 更新账户使用额度(如果设置了额度限制)
|
||||||
if (parseFloat(account.dailyQuota) > 0) {
|
if (parseFloat(account.dailyQuota) > 0) {
|
||||||
// 估算费用(根据模型和token数量)
|
// 使用CostCalculator正确计算费用(考虑缓存token的不同价格)
|
||||||
const estimatedCost = this._estimateCost(modelToRecord, inputTokens, outputTokens)
|
const CostCalculator = require('../utils/costCalculator')
|
||||||
await openaiResponsesAccountService.updateUsageQuota(account.id, estimatedCost)
|
const costInfo = CostCalculator.calculateCost(
|
||||||
|
{
|
||||||
|
input_tokens: actualInputTokens, // 实际输入(不含缓存)
|
||||||
|
output_tokens: outputTokens,
|
||||||
|
cache_creation_input_tokens: 0, // OpenAI没有cache_creation
|
||||||
|
cache_read_input_tokens: cacheReadTokens
|
||||||
|
},
|
||||||
|
modelToRecord
|
||||||
|
)
|
||||||
|
await openaiResponsesAccountService.updateUsageQuota(account.id, costInfo.costs.total)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to record usage:', error)
|
logger.error('Failed to record usage:', error)
|
||||||
@@ -502,27 +512,28 @@ class OpenAIResponsesRelayService {
|
|||||||
if (usageData) {
|
if (usageData) {
|
||||||
try {
|
try {
|
||||||
// OpenAI-Responses 使用 input_tokens/output_tokens,标准 OpenAI 使用 prompt_tokens/completion_tokens
|
// OpenAI-Responses 使用 input_tokens/output_tokens,标准 OpenAI 使用 prompt_tokens/completion_tokens
|
||||||
const inputTokens = usageData.input_tokens || usageData.prompt_tokens || 0
|
const totalInputTokens = usageData.input_tokens || usageData.prompt_tokens || 0
|
||||||
const outputTokens = usageData.output_tokens || usageData.completion_tokens || 0
|
const outputTokens = usageData.output_tokens || usageData.completion_tokens || 0
|
||||||
|
|
||||||
// 提取缓存相关的 tokens(如果存在)
|
// 提取缓存相关的 tokens(如果存在)
|
||||||
const cacheCreateTokens = usageData.input_tokens_details?.cache_creation_tokens || 0
|
|
||||||
const cacheReadTokens = usageData.input_tokens_details?.cached_tokens || 0
|
const cacheReadTokens = usageData.input_tokens_details?.cached_tokens || 0
|
||||||
|
// 计算实际输入token(总输入减去缓存部分)
|
||||||
|
const actualInputTokens = Math.max(0, totalInputTokens - cacheReadTokens)
|
||||||
|
|
||||||
const totalTokens = usageData.total_tokens || inputTokens + outputTokens
|
const totalTokens = usageData.total_tokens || totalInputTokens + outputTokens
|
||||||
|
|
||||||
await apiKeyService.recordUsage(
|
await apiKeyService.recordUsage(
|
||||||
apiKeyData.id,
|
apiKeyData.id,
|
||||||
inputTokens,
|
actualInputTokens, // 传递实际输入(不含缓存)
|
||||||
outputTokens,
|
outputTokens,
|
||||||
cacheCreateTokens,
|
0, // OpenAI没有cache_creation_tokens
|
||||||
cacheReadTokens,
|
cacheReadTokens,
|
||||||
actualModel,
|
actualModel,
|
||||||
account.id
|
account.id
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`📊 Recorded non-stream usage - Input: ${inputTokens}, Output: ${outputTokens}, CacheRead: ${cacheReadTokens}, CacheCreate: ${cacheCreateTokens}, Total: ${totalTokens}, Model: ${actualModel}`
|
`📊 Recorded non-stream usage - Input: ${totalInputTokens}(actual:${actualInputTokens}+cached:${cacheReadTokens}), Output: ${outputTokens}, Total: ${totalTokens}, Model: ${actualModel}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// 更新账户的 token 使用统计
|
// 更新账户的 token 使用统计
|
||||||
@@ -530,9 +541,18 @@ class OpenAIResponsesRelayService {
|
|||||||
|
|
||||||
// 更新账户使用额度(如果设置了额度限制)
|
// 更新账户使用额度(如果设置了额度限制)
|
||||||
if (parseFloat(account.dailyQuota) > 0) {
|
if (parseFloat(account.dailyQuota) > 0) {
|
||||||
// 估算费用(根据模型和token数量)
|
// 使用CostCalculator正确计算费用(考虑缓存token的不同价格)
|
||||||
const estimatedCost = this._estimateCost(actualModel, inputTokens, outputTokens)
|
const CostCalculator = require('../utils/costCalculator')
|
||||||
await openaiResponsesAccountService.updateUsageQuota(account.id, estimatedCost)
|
const costInfo = CostCalculator.calculateCost(
|
||||||
|
{
|
||||||
|
input_tokens: actualInputTokens, // 实际输入(不含缓存)
|
||||||
|
output_tokens: outputTokens,
|
||||||
|
cache_creation_input_tokens: 0, // OpenAI没有cache_creation
|
||||||
|
cache_read_input_tokens: cacheReadTokens
|
||||||
|
},
|
||||||
|
actualModel
|
||||||
|
)
|
||||||
|
await openaiResponsesAccountService.updateUsageQuota(account.id, costInfo.costs.total)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to record usage:', error)
|
logger.error('Failed to record usage:', error)
|
||||||
|
|||||||
@@ -215,7 +215,7 @@
|
|||||||
class="text-orange-600"
|
class="text-orange-600"
|
||||||
>
|
>
|
||||||
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
<i class="fas fa-exclamation-triangle mr-1 text-xs md:text-sm" />
|
||||||
限制 {{ statsData.restrictions.allowedClients.length }} 个客户端
|
限 {{ statsData.restrictions.allowedClients.length }} 种客户端使用
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-green-600">
|
<span v-else class="text-green-600">
|
||||||
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
<i class="fas fa-check-circle mr-1 text-xs md:text-sm" />
|
||||||
|
|||||||
Reference in New Issue
Block a user