fix: 修复openai输入token计算问题

This commit is contained in:
shaw
2025-09-20 21:43:48 +08:00
parent 08c2b7a444
commit 3628bb2b7a
4 changed files with 51 additions and 186 deletions

View File

@@ -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
})

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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" />