mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
Merge remote-tracking branch 'upstream/main'
# Conflicts: # src/routes/api.js
This commit is contained in:
21
README.md
21
README.md
@@ -473,23 +473,23 @@ Cherry Studio支持多种AI服务的接入,下面是不同账号类型的详
|
|||||||
|
|
||||||
```
|
```
|
||||||
# API地址
|
# API地址
|
||||||
http://你的服务器:3000/claude/
|
http://你的服务器:3000/claude
|
||||||
|
|
||||||
# 模型ID示例
|
# 模型ID示例
|
||||||
claude-sonnet-4-20250514 # Claude Sonnet 4
|
claude-sonnet-4-5-20250929 # Claude Sonnet 4.5
|
||||||
claude-opus-4-20250514 # Claude Opus 4
|
claude-opus-4-20250514 # Claude Opus 4
|
||||||
```
|
```
|
||||||
|
|
||||||
配置步骤:
|
配置步骤:
|
||||||
- 供应商类型选择"Anthropic"
|
- 供应商类型选择"Anthropic"
|
||||||
- API地址填入:`http://你的服务器:3000/claude/`
|
- API地址填入:`http://你的服务器:3000/claude`
|
||||||
- API Key填入:后台创建的API密钥(cr_开头)
|
- API Key填入:后台创建的API密钥(cr_开头)
|
||||||
|
|
||||||
**2. Gemini账号接入:**
|
**2. Gemini账号接入:**
|
||||||
|
|
||||||
```
|
```
|
||||||
# API地址
|
# API地址
|
||||||
http://你的服务器:3000/gemini/
|
http://你的服务器:3000/gemini
|
||||||
|
|
||||||
# 模型ID示例
|
# 模型ID示例
|
||||||
gemini-2.5-pro # Gemini 2.5 Pro
|
gemini-2.5-pro # Gemini 2.5 Pro
|
||||||
@@ -497,14 +497,14 @@ gemini-2.5-pro # Gemini 2.5 Pro
|
|||||||
|
|
||||||
配置步骤:
|
配置步骤:
|
||||||
- 供应商类型选择"Gemini"
|
- 供应商类型选择"Gemini"
|
||||||
- API地址填入:`http://你的服务器:3000/gemini/`
|
- API地址填入:`http://你的服务器:3000/gemini`
|
||||||
- API Key填入:后台创建的API密钥(cr_开头)
|
- API Key填入:后台创建的API密钥(cr_开头)
|
||||||
|
|
||||||
**3. Codex接入:**
|
**3. Codex接入:**
|
||||||
|
|
||||||
```
|
```
|
||||||
# API地址
|
# API地址
|
||||||
http://你的服务器:3000/openai/
|
http://你的服务器:3000/openai
|
||||||
|
|
||||||
# 模型ID(固定)
|
# 模型ID(固定)
|
||||||
gpt-5 # Codex使用固定模型ID
|
gpt-5 # Codex使用固定模型ID
|
||||||
@@ -512,10 +512,17 @@ gpt-5 # Codex使用固定模型ID
|
|||||||
|
|
||||||
配置步骤:
|
配置步骤:
|
||||||
- 供应商类型选择"Openai-Response"
|
- 供应商类型选择"Openai-Response"
|
||||||
- API地址填入:`http://你的服务器:3000/openai/`
|
- API地址填入:`http://你的服务器:3000/openai`
|
||||||
- API Key填入:后台创建的API密钥(cr_开头)
|
- API Key填入:后台创建的API密钥(cr_开头)
|
||||||
- **重要**:Codex只支持Openai-Response标准
|
- **重要**:Codex只支持Openai-Response标准
|
||||||
|
|
||||||
|
**Cherry Studio 地址格式重要说明:**
|
||||||
|
|
||||||
|
- ✅ **推荐格式**:`http://你的服务器:3000/claude`(不加结尾 `/`,让 Cherry Studio 自动加上 v1)
|
||||||
|
- ✅ **等效格式**:`http://你的服务器:3000/claude/v1/`(手动指定 v1 并加结尾 `/`)
|
||||||
|
- 💡 **说明**:这两种格式在 Cherry Studio 中是完全等效的
|
||||||
|
- ❌ **错误格式**:`http://你的服务器:3000/claude/`(单独的 `/` 结尾会被 Cherry Studio 忽略 v1 版本)
|
||||||
|
|
||||||
#### 其他第三方工具接入
|
#### 其他第三方工具接入
|
||||||
|
|
||||||
**接入要点:**
|
**接入要点:**
|
||||||
|
|||||||
@@ -4162,6 +4162,36 @@ router.get('/accounts/:accountId/usage-history', authenticateAdmin, async (req,
|
|||||||
gemini: 'gemini-1.5-flash'
|
gemini: 'gemini-1.5-flash'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取账户信息以获取创建时间
|
||||||
|
let accountData = null
|
||||||
|
let accountCreatedAt = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (platform) {
|
||||||
|
case 'claude':
|
||||||
|
accountData = await claudeAccountService.getAccount(accountId)
|
||||||
|
break
|
||||||
|
case 'claude-console':
|
||||||
|
accountData = await claudeConsoleAccountService.getAccount(accountId)
|
||||||
|
break
|
||||||
|
case 'openai':
|
||||||
|
accountData = await openaiAccountService.getAccount(accountId)
|
||||||
|
break
|
||||||
|
case 'openai-responses':
|
||||||
|
accountData = await openaiResponsesAccountService.getAccount(accountId)
|
||||||
|
break
|
||||||
|
case 'gemini':
|
||||||
|
accountData = await geminiAccountService.getAccount(accountId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountData && accountData.createdAt) {
|
||||||
|
accountCreatedAt = new Date(accountData.createdAt)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to get account data for avgDailyCost calculation: ${error.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const fallbackModel = fallbackModelMap[platform] || 'unknown'
|
const fallbackModel = fallbackModelMap[platform] || 'unknown'
|
||||||
const daysCount = Math.min(Math.max(parseInt(days, 10) || 30, 1), 60)
|
const daysCount = Math.min(Math.max(parseInt(days, 10) || 30, 1), 60)
|
||||||
@@ -4281,9 +4311,22 @@ router.get('/accounts/:accountId/usage-history', authenticateAdmin, async (req,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const avgDailyCost = daysCount > 0 ? totalCost / daysCount : 0
|
// 计算实际使用天数(从账户创建到现在)
|
||||||
const avgDailyRequests = daysCount > 0 ? totalRequests / daysCount : 0
|
let actualDaysForAvg = daysCount
|
||||||
const avgDailyTokens = daysCount > 0 ? totalTokens / daysCount : 0
|
if (accountCreatedAt) {
|
||||||
|
const now = new Date()
|
||||||
|
const diffTime = Math.abs(now - accountCreatedAt)
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||||
|
// 使用实际使用天数,但不超过请求的天数范围
|
||||||
|
actualDaysForAvg = Math.min(diffDays, daysCount)
|
||||||
|
// 至少为1天,避免除零
|
||||||
|
actualDaysForAvg = Math.max(actualDaysForAvg, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用实际天数计算日均值
|
||||||
|
const avgDailyCost = actualDaysForAvg > 0 ? totalCost / actualDaysForAvg : 0
|
||||||
|
const avgDailyRequests = actualDaysForAvg > 0 ? totalRequests / actualDaysForAvg : 0
|
||||||
|
const avgDailyTokens = actualDaysForAvg > 0 ? totalTokens / actualDaysForAvg : 0
|
||||||
|
|
||||||
const todayData = history.length > 0 ? history[history.length - 1] : null
|
const todayData = history.length > 0 ? history[history.length - 1] : null
|
||||||
|
|
||||||
@@ -4293,6 +4336,8 @@ router.get('/accounts/:accountId/usage-history', authenticateAdmin, async (req,
|
|||||||
history,
|
history,
|
||||||
summary: {
|
summary: {
|
||||||
days: daysCount,
|
days: daysCount,
|
||||||
|
actualDaysUsed: actualDaysForAvg, // 实际使用的天数(用于计算日均值)
|
||||||
|
accountCreatedAt: accountCreatedAt ? accountCreatedAt.toISOString() : null,
|
||||||
totalCost,
|
totalCost,
|
||||||
totalCostFormatted: CostCalculator.formatCost(totalCost),
|
totalCostFormatted: CostCalculator.formatCost(totalCost),
|
||||||
totalRequests,
|
totalRequests,
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ const ccrRelayService = require('../services/ccrRelayService')
|
|||||||
const bedrockAccountService = require('../services/bedrockAccountService')
|
const bedrockAccountService = require('../services/bedrockAccountService')
|
||||||
const unifiedClaudeScheduler = require('../services/unifiedClaudeScheduler')
|
const unifiedClaudeScheduler = require('../services/unifiedClaudeScheduler')
|
||||||
const apiKeyService = require('../services/apiKeyService')
|
const apiKeyService = require('../services/apiKeyService')
|
||||||
const pricingService = require('../services/pricingService')
|
|
||||||
const { authenticateApiKey } = require('../middleware/auth')
|
const { authenticateApiKey } = require('../middleware/auth')
|
||||||
const logger = require('../utils/logger')
|
const logger = require('../utils/logger')
|
||||||
const redis = require('../models/redis')
|
|
||||||
const { getEffectiveModel, parseVendorPrefixedModel } = require('../utils/modelHelper')
|
const { getEffectiveModel, parseVendorPrefixedModel } = require('../utils/modelHelper')
|
||||||
const sessionHelper = require('../utils/sessionHelper')
|
const sessionHelper = require('../utils/sessionHelper')
|
||||||
const openaiToClaude = require('../services/openaiToClaude')
|
const openaiToClaude = require('../services/openaiToClaude')
|
||||||
@@ -34,6 +32,33 @@ function detectBackendFromModel(modelName) {
|
|||||||
return 'claude' // 默认使用 Claude
|
return 'claude' // 默认使用 Claude
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
function queueRateLimitUpdate(rateLimitInfo, usageSummary, model, context = '') {
|
||||||
|
if (!rateLimitInfo) {
|
||||||
|
return Promise.resolve({ totalTokens: 0, totalCost: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = context ? ` (${context})` : ''
|
||||||
|
|
||||||
|
return updateRateLimitCounters(rateLimitInfo, usageSummary, model)
|
||||||
|
.then(({ totalTokens, totalCost }) => {
|
||||||
|
if (totalTokens > 0) {
|
||||||
|
logger.api(`📊 Updated rate limit token count${label}: +${totalTokens} tokens`)
|
||||||
|
}
|
||||||
|
if (typeof totalCost === 'number' && totalCost > 0) {
|
||||||
|
logger.api(`💰 Updated rate limit cost count${label}: +$${totalCost.toFixed(6)}`)
|
||||||
|
}
|
||||||
|
return { totalTokens, totalCost }
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(`❌ Failed to update rate limit counters${label}:`, error)
|
||||||
|
return { totalTokens: 0, totalCost: 0 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 🔧 共享的消息处理函数
|
// 🔧 共享的消息处理函数
|
||||||
async function handleMessagesRequest(req, res) {
|
async function handleMessagesRequest(req, res) {
|
||||||
try {
|
try {
|
||||||
@@ -210,35 +235,17 @@ async function handleMessagesRequest(req, res) {
|
|||||||
logger.error('❌ Failed to record stream usage:', error)
|
logger.error('❌ Failed to record stream usage:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新时间窗口内的token计数和费用
|
queueRateLimitUpdate(
|
||||||
if (req.rateLimitInfo) {
|
req.rateLimitInfo,
|
||||||
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
|
{
|
||||||
|
inputTokens,
|
||||||
// 更新Token计数(向后兼容)
|
outputTokens,
|
||||||
redis
|
cacheCreateTokens,
|
||||||
.getClient()
|
cacheReadTokens
|
||||||
.incrby(req.rateLimitInfo.tokenCountKey, totalTokens)
|
},
|
||||||
.catch((error) => {
|
model,
|
||||||
logger.error('❌ Failed to update rate limit token count:', error)
|
'claude-stream'
|
||||||
})
|
|
||||||
logger.api(`📊 Updated rate limit token count: +${totalTokens} tokens`)
|
|
||||||
|
|
||||||
// 计算并更新费用计数(新功能)
|
|
||||||
if (req.rateLimitInfo.costCountKey) {
|
|
||||||
const costInfo = pricingService.calculateCost(usageData, model)
|
|
||||||
if (costInfo.totalCost > 0) {
|
|
||||||
redis
|
|
||||||
.getClient()
|
|
||||||
.incrbyfloat(req.rateLimitInfo.costCountKey, costInfo.totalCost)
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('❌ Failed to update rate limit cost count:', error)
|
|
||||||
})
|
|
||||||
logger.api(
|
|
||||||
`💰 Updated rate limit cost count: +$${costInfo.totalCost.toFixed(6)}`
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usageDataCaptured = true
|
usageDataCaptured = true
|
||||||
logger.api(
|
logger.api(
|
||||||
@@ -319,35 +326,17 @@ async function handleMessagesRequest(req, res) {
|
|||||||
logger.error('❌ Failed to record stream usage:', error)
|
logger.error('❌ Failed to record stream usage:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新时间窗口内的token计数和费用
|
queueRateLimitUpdate(
|
||||||
if (req.rateLimitInfo) {
|
req.rateLimitInfo,
|
||||||
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
|
{
|
||||||
|
inputTokens,
|
||||||
// 更新Token计数(向后兼容)
|
outputTokens,
|
||||||
redis
|
cacheCreateTokens,
|
||||||
.getClient()
|
cacheReadTokens
|
||||||
.incrby(req.rateLimitInfo.tokenCountKey, totalTokens)
|
},
|
||||||
.catch((error) => {
|
model,
|
||||||
logger.error('❌ Failed to update rate limit token count:', error)
|
'claude-console-stream'
|
||||||
})
|
|
||||||
logger.api(`📊 Updated rate limit token count: +${totalTokens} tokens`)
|
|
||||||
|
|
||||||
// 计算并更新费用计数(新功能)
|
|
||||||
if (req.rateLimitInfo.costCountKey) {
|
|
||||||
const costInfo = pricingService.calculateCost(usageData, model)
|
|
||||||
if (costInfo.totalCost > 0) {
|
|
||||||
redis
|
|
||||||
.getClient()
|
|
||||||
.incrbyfloat(req.rateLimitInfo.costCountKey, costInfo.totalCost)
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('❌ Failed to update rate limit cost count:', error)
|
|
||||||
})
|
|
||||||
logger.api(
|
|
||||||
`💰 Updated rate limit cost count: +$${costInfo.totalCost.toFixed(6)}`
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usageDataCaptured = true
|
usageDataCaptured = true
|
||||||
logger.api(
|
logger.api(
|
||||||
@@ -387,33 +376,17 @@ async function handleMessagesRequest(req, res) {
|
|||||||
logger.error('❌ Failed to record Bedrock stream usage:', error)
|
logger.error('❌ Failed to record Bedrock stream usage:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新时间窗口内的token计数和费用
|
queueRateLimitUpdate(
|
||||||
if (req.rateLimitInfo) {
|
req.rateLimitInfo,
|
||||||
const totalTokens = inputTokens + outputTokens
|
{
|
||||||
|
inputTokens,
|
||||||
// 更新Token计数(向后兼容)
|
outputTokens,
|
||||||
redis
|
cacheCreateTokens: 0,
|
||||||
.getClient()
|
cacheReadTokens: 0
|
||||||
.incrby(req.rateLimitInfo.tokenCountKey, totalTokens)
|
},
|
||||||
.catch((error) => {
|
result.model,
|
||||||
logger.error('❌ Failed to update rate limit token count:', error)
|
'bedrock-stream'
|
||||||
})
|
)
|
||||||
logger.api(`📊 Updated rate limit token count: +${totalTokens} tokens`)
|
|
||||||
|
|
||||||
// 计算并更新费用计数(新功能)
|
|
||||||
if (req.rateLimitInfo.costCountKey) {
|
|
||||||
const costInfo = pricingService.calculateCost(result.usage, result.model)
|
|
||||||
if (costInfo.totalCost > 0) {
|
|
||||||
redis
|
|
||||||
.getClient()
|
|
||||||
.incrbyfloat(req.rateLimitInfo.costCountKey, costInfo.totalCost)
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('❌ Failed to update rate limit cost count:', error)
|
|
||||||
})
|
|
||||||
logger.api(`💰 Updated rate limit cost count: +$${costInfo.totalCost.toFixed(6)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usageDataCaptured = true
|
usageDataCaptured = true
|
||||||
logger.api(
|
logger.api(
|
||||||
@@ -488,35 +461,17 @@ async function handleMessagesRequest(req, res) {
|
|||||||
logger.error('❌ Failed to record CCR stream usage:', error)
|
logger.error('❌ Failed to record CCR stream usage:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新时间窗口内的token计数和费用
|
queueRateLimitUpdate(
|
||||||
if (req.rateLimitInfo) {
|
req.rateLimitInfo,
|
||||||
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
|
{
|
||||||
|
inputTokens,
|
||||||
// 更新Token计数(向后兼容)
|
outputTokens,
|
||||||
redis
|
cacheCreateTokens,
|
||||||
.getClient()
|
cacheReadTokens
|
||||||
.incrby(req.rateLimitInfo.tokenCountKey, totalTokens)
|
},
|
||||||
.catch((error) => {
|
model,
|
||||||
logger.error('❌ Failed to update rate limit token count:', error)
|
'ccr-stream'
|
||||||
})
|
|
||||||
logger.api(`📊 Updated rate limit token count: +${totalTokens} tokens`)
|
|
||||||
|
|
||||||
// 计算并更新费用计数(新功能)
|
|
||||||
if (req.rateLimitInfo.costCountKey) {
|
|
||||||
const costInfo = pricingService.calculateCost(usageData, model)
|
|
||||||
if (costInfo.totalCost > 0) {
|
|
||||||
redis
|
|
||||||
.getClient()
|
|
||||||
.incrbyfloat(req.rateLimitInfo.costCountKey, costInfo.totalCost)
|
|
||||||
.catch((error) => {
|
|
||||||
logger.error('❌ Failed to update rate limit cost count:', error)
|
|
||||||
})
|
|
||||||
logger.api(
|
|
||||||
`💰 Updated rate limit cost count: +$${costInfo.totalCost.toFixed(6)}`
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usageDataCaptured = true
|
usageDataCaptured = true
|
||||||
logger.api(
|
logger.api(
|
||||||
@@ -704,25 +659,17 @@ async function handleMessagesRequest(req, res) {
|
|||||||
responseAccountId
|
responseAccountId
|
||||||
)
|
)
|
||||||
|
|
||||||
// 更新时间窗口内的token计数和费用
|
await queueRateLimitUpdate(
|
||||||
if (req.rateLimitInfo) {
|
req.rateLimitInfo,
|
||||||
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
|
{
|
||||||
|
inputTokens,
|
||||||
// 更新Token计数(向后兼容)
|
outputTokens,
|
||||||
await redis.getClient().incrby(req.rateLimitInfo.tokenCountKey, totalTokens)
|
cacheCreateTokens,
|
||||||
logger.api(`📊 Updated rate limit token count: +${totalTokens} tokens`)
|
cacheReadTokens
|
||||||
|
},
|
||||||
// 计算并更新费用计数(新功能)
|
model,
|
||||||
if (req.rateLimitInfo.costCountKey) {
|
'claude-non-stream'
|
||||||
const costInfo = pricingService.calculateCost(jsonData.usage, model)
|
)
|
||||||
if (costInfo.totalCost > 0) {
|
|
||||||
await redis
|
|
||||||
.getClient()
|
|
||||||
.incrbyfloat(req.rateLimitInfo.costCountKey, costInfo.totalCost)
|
|
||||||
logger.api(`💰 Updated rate limit cost count: +$${costInfo.totalCost.toFixed(6)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usageRecorded = true
|
usageRecorded = true
|
||||||
logger.api(
|
logger.api(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const crypto = require('crypto')
|
|||||||
const sessionHelper = require('../utils/sessionHelper')
|
const sessionHelper = require('../utils/sessionHelper')
|
||||||
const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler')
|
const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler')
|
||||||
const apiKeyService = require('../services/apiKeyService')
|
const apiKeyService = require('../services/apiKeyService')
|
||||||
|
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
|
||||||
// const { OAuth2Client } = require('google-auth-library'); // OAuth2Client is not used in this file
|
// const { OAuth2Client } = require('google-auth-library'); // OAuth2Client is not used in this file
|
||||||
|
|
||||||
// 生成会话哈希
|
// 生成会话哈希
|
||||||
@@ -49,6 +50,31 @@ function ensureGeminiPermission(req, res) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyRateLimitTracking(req, usageSummary, model, context = '') {
|
||||||
|
if (!req.rateLimitInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = context ? ` (${context})` : ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { totalTokens, totalCost } = await updateRateLimitCounters(
|
||||||
|
req.rateLimitInfo,
|
||||||
|
usageSummary,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
|
||||||
|
if (totalTokens > 0) {
|
||||||
|
logger.api(`📊 Updated rate limit token count${label}: +${totalTokens} tokens`)
|
||||||
|
}
|
||||||
|
if (typeof totalCost === 'number' && totalCost > 0) {
|
||||||
|
logger.api(`💰 Updated rate limit cost count${label}: +$${totalCost.toFixed(6)}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to update rate limit counters${label}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Gemini 消息处理端点
|
// Gemini 消息处理端点
|
||||||
router.post('/messages', authenticateApiKey, async (req, res) => {
|
router.post('/messages', authenticateApiKey, async (req, res) => {
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
@@ -679,6 +705,18 @@ async function handleGenerateContent(req, res) {
|
|||||||
logger.info(
|
logger.info(
|
||||||
`📊 Recorded Gemini usage - Input: ${usage.promptTokenCount}, Output: ${usage.candidatesTokenCount}, Total: ${usage.totalTokenCount}`
|
`📊 Recorded Gemini usage - Input: ${usage.promptTokenCount}, Output: ${usage.candidatesTokenCount}, Total: ${usage.totalTokenCount}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await applyRateLimitTracking(
|
||||||
|
req,
|
||||||
|
{
|
||||||
|
inputTokens: usage.promptTokenCount || 0,
|
||||||
|
outputTokens: usage.candidatesTokenCount || 0,
|
||||||
|
cacheCreateTokens: 0,
|
||||||
|
cacheReadTokens: 0
|
||||||
|
},
|
||||||
|
model,
|
||||||
|
'gemini-non-stream'
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to record Gemini usage:', error)
|
logger.error('Failed to record Gemini usage:', error)
|
||||||
}
|
}
|
||||||
@@ -935,6 +973,18 @@ async function handleStreamGenerateContent(req, res) {
|
|||||||
logger.info(
|
logger.info(
|
||||||
`📊 Recorded Gemini stream usage - Input: ${totalUsage.promptTokenCount}, Output: ${totalUsage.candidatesTokenCount}, Total: ${totalUsage.totalTokenCount}`
|
`📊 Recorded Gemini stream usage - Input: ${totalUsage.promptTokenCount}, Output: ${totalUsage.candidatesTokenCount}, Total: ${totalUsage.totalTokenCount}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await applyRateLimitTracking(
|
||||||
|
req,
|
||||||
|
{
|
||||||
|
inputTokens: totalUsage.promptTokenCount || 0,
|
||||||
|
outputTokens: totalUsage.candidatesTokenCount || 0,
|
||||||
|
cacheCreateTokens: 0,
|
||||||
|
cacheReadTokens: 0
|
||||||
|
},
|
||||||
|
model,
|
||||||
|
'gemini-stream'
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to record Gemini usage:', error)
|
logger.error('Failed to record Gemini usage:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const apiKeyService = require('../services/apiKeyService')
|
|||||||
const unifiedClaudeScheduler = require('../services/unifiedClaudeScheduler')
|
const unifiedClaudeScheduler = require('../services/unifiedClaudeScheduler')
|
||||||
const claudeCodeHeadersService = require('../services/claudeCodeHeadersService')
|
const claudeCodeHeadersService = require('../services/claudeCodeHeadersService')
|
||||||
const sessionHelper = require('../utils/sessionHelper')
|
const sessionHelper = require('../utils/sessionHelper')
|
||||||
|
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
|
||||||
|
|
||||||
// 加载模型定价数据
|
// 加载模型定价数据
|
||||||
let modelPricingData = {}
|
let modelPricingData = {}
|
||||||
@@ -33,6 +34,27 @@ function checkPermissions(apiKeyData, requiredPermission = 'claude') {
|
|||||||
return permissions === 'all' || permissions === requiredPermission
|
return permissions === 'all' || permissions === requiredPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function queueRateLimitUpdate(rateLimitInfo, usageSummary, model, context = '') {
|
||||||
|
if (!rateLimitInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = context ? ` (${context})` : ''
|
||||||
|
|
||||||
|
updateRateLimitCounters(rateLimitInfo, usageSummary, model)
|
||||||
|
.then(({ totalTokens, totalCost }) => {
|
||||||
|
if (totalTokens > 0) {
|
||||||
|
logger.api(`📊 Updated rate limit token count${label}: +${totalTokens} tokens`)
|
||||||
|
}
|
||||||
|
if (typeof totalCost === 'number' && totalCost > 0) {
|
||||||
|
logger.api(`💰 Updated rate limit cost count${label}: +$${totalCost.toFixed(6)}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(`❌ Failed to update rate limit counters${label}:`, error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 📋 OpenAI 兼容的模型列表端点
|
// 📋 OpenAI 兼容的模型列表端点
|
||||||
router.get('/v1/models', authenticateApiKey, async (req, res) => {
|
router.get('/v1/models', authenticateApiKey, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -263,6 +285,12 @@ async function handleChatCompletion(req, res, apiKeyData) {
|
|||||||
// 记录使用统计
|
// 记录使用统计
|
||||||
if (usage && usage.input_tokens !== undefined && usage.output_tokens !== undefined) {
|
if (usage && usage.input_tokens !== undefined && usage.output_tokens !== undefined) {
|
||||||
const model = usage.model || claudeRequest.model
|
const model = usage.model || claudeRequest.model
|
||||||
|
const cacheCreateTokens =
|
||||||
|
(usage.cache_creation && typeof usage.cache_creation === 'object'
|
||||||
|
? (usage.cache_creation.ephemeral_5m_input_tokens || 0) +
|
||||||
|
(usage.cache_creation.ephemeral_1h_input_tokens || 0)
|
||||||
|
: usage.cache_creation_input_tokens || 0) || 0
|
||||||
|
const cacheReadTokens = usage.cache_read_input_tokens || 0
|
||||||
|
|
||||||
// 使用新的 recordUsageWithDetails 方法来支持详细的缓存数据
|
// 使用新的 recordUsageWithDetails 方法来支持详细的缓存数据
|
||||||
apiKeyService
|
apiKeyService
|
||||||
@@ -275,6 +303,18 @@ async function handleChatCompletion(req, res, apiKeyData) {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error('❌ Failed to record usage:', error)
|
logger.error('❌ Failed to record usage:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queueRateLimitUpdate(
|
||||||
|
req.rateLimitInfo,
|
||||||
|
{
|
||||||
|
inputTokens: usage.input_tokens || 0,
|
||||||
|
outputTokens: usage.output_tokens || 0,
|
||||||
|
cacheCreateTokens,
|
||||||
|
cacheReadTokens
|
||||||
|
},
|
||||||
|
model,
|
||||||
|
'openai-claude-stream'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 流转换器
|
// 流转换器
|
||||||
@@ -334,6 +374,12 @@ async function handleChatCompletion(req, res, apiKeyData) {
|
|||||||
// 记录使用统计
|
// 记录使用统计
|
||||||
if (claudeData.usage) {
|
if (claudeData.usage) {
|
||||||
const { usage } = claudeData
|
const { usage } = claudeData
|
||||||
|
const cacheCreateTokens =
|
||||||
|
(usage.cache_creation && typeof usage.cache_creation === 'object'
|
||||||
|
? (usage.cache_creation.ephemeral_5m_input_tokens || 0) +
|
||||||
|
(usage.cache_creation.ephemeral_1h_input_tokens || 0)
|
||||||
|
: usage.cache_creation_input_tokens || 0) || 0
|
||||||
|
const cacheReadTokens = usage.cache_read_input_tokens || 0
|
||||||
// 使用新的 recordUsageWithDetails 方法来支持详细的缓存数据
|
// 使用新的 recordUsageWithDetails 方法来支持详细的缓存数据
|
||||||
apiKeyService
|
apiKeyService
|
||||||
.recordUsageWithDetails(
|
.recordUsageWithDetails(
|
||||||
@@ -345,6 +391,18 @@ async function handleChatCompletion(req, res, apiKeyData) {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error('❌ Failed to record usage:', error)
|
logger.error('❌ Failed to record usage:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queueRateLimitUpdate(
|
||||||
|
req.rateLimitInfo,
|
||||||
|
{
|
||||||
|
inputTokens: usage.input_tokens || 0,
|
||||||
|
outputTokens: usage.output_tokens || 0,
|
||||||
|
cacheCreateTokens,
|
||||||
|
cacheReadTokens
|
||||||
|
},
|
||||||
|
claudeRequest.model,
|
||||||
|
'openai-claude-non-stream'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回 OpenAI 格式响应
|
// 返回 OpenAI 格式响应
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const openaiResponsesRelayService = require('../services/openaiResponsesRelaySer
|
|||||||
const apiKeyService = require('../services/apiKeyService')
|
const apiKeyService = require('../services/apiKeyService')
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const ProxyHelper = require('../utils/proxyHelper')
|
const ProxyHelper = require('../utils/proxyHelper')
|
||||||
|
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
|
||||||
|
|
||||||
// 创建代理 Agent(使用统一的代理工具)
|
// 创建代理 Agent(使用统一的代理工具)
|
||||||
function createProxyAgent(proxy) {
|
function createProxyAgent(proxy) {
|
||||||
@@ -67,6 +68,31 @@ function extractCodexUsageHeaders(headers) {
|
|||||||
return hasData ? snapshot : null
|
return hasData ? snapshot : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyRateLimitTracking(req, usageSummary, model, context = '') {
|
||||||
|
if (!req.rateLimitInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = context ? ` (${context})` : ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { totalTokens, totalCost } = await updateRateLimitCounters(
|
||||||
|
req.rateLimitInfo,
|
||||||
|
usageSummary,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
|
||||||
|
if (totalTokens > 0) {
|
||||||
|
logger.api(`📊 Updated rate limit token count${label}: +${totalTokens} tokens`)
|
||||||
|
}
|
||||||
|
if (typeof totalCost === 'number' && totalCost > 0) {
|
||||||
|
logger.api(`💰 Updated rate limit cost count${label}: +$${totalCost.toFixed(6)}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ Failed to update rate limit counters${label}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 使用统一调度器选择 OpenAI 账户
|
// 使用统一调度器选择 OpenAI 账户
|
||||||
async function getOpenAIAuthToken(apiKeyData, sessionId = null, requestedModel = null) {
|
async function getOpenAIAuthToken(apiKeyData, sessionId = null, requestedModel = null) {
|
||||||
try {
|
try {
|
||||||
@@ -579,6 +605,18 @@ const handleResponses = async (req, res) => {
|
|||||||
logger.info(
|
logger.info(
|
||||||
`📊 Recorded OpenAI non-stream usage - Input: ${totalInputTokens}(actual:${actualInputTokens}+cached:${cacheReadTokens}), Output: ${outputTokens}, Total: ${usageData.total_tokens || totalInputTokens + outputTokens}, Model: ${actualModel}`
|
`📊 Recorded OpenAI non-stream usage - Input: ${totalInputTokens}(actual:${actualInputTokens}+cached:${cacheReadTokens}), Output: ${outputTokens}, Total: ${usageData.total_tokens || totalInputTokens + outputTokens}, Model: ${actualModel}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await applyRateLimitTracking(
|
||||||
|
req,
|
||||||
|
{
|
||||||
|
inputTokens: actualInputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheCreateTokens: 0,
|
||||||
|
cacheReadTokens
|
||||||
|
},
|
||||||
|
actualModel,
|
||||||
|
'openai-non-stream'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回响应
|
// 返回响应
|
||||||
@@ -700,6 +738,18 @@ const handleResponses = async (req, res) => {
|
|||||||
`📊 Recorded OpenAI usage - Input: ${totalInputTokens}(actual:${actualInputTokens}+cached:${cacheReadTokens}), Output: ${outputTokens}, Total: ${usageData.total_tokens || totalInputTokens + 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
|
||||||
|
|
||||||
|
await applyRateLimitTracking(
|
||||||
|
req,
|
||||||
|
{
|
||||||
|
inputTokens: actualInputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheCreateTokens: 0,
|
||||||
|
cacheReadTokens
|
||||||
|
},
|
||||||
|
modelToRecord,
|
||||||
|
'openai-stream'
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to record OpenAI usage:', error)
|
logger.error('Failed to record OpenAI usage:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,17 +39,8 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔍 判断是否是真实的 Claude Code 请求
|
// 🔍 判断是否是真实的 Claude Code 请求
|
||||||
isRealClaudeCodeRequest(requestBody, clientHeaders) {
|
isRealClaudeCodeRequest(requestBody) {
|
||||||
// 使用 claudeCodeValidator 来进行完整的验证
|
return ClaudeCodeValidator.hasClaudeCodeSystemPrompt(requestBody)
|
||||||
// 注意:claudeCodeValidator.validate() 需要一个完整的 req 对象
|
|
||||||
// 我们需要构造一个最小化的 req 对象来满足验证器的需求
|
|
||||||
const mockReq = {
|
|
||||||
headers: clientHeaders || {},
|
|
||||||
body: requestBody,
|
|
||||||
path: '/api/v1/messages'
|
|
||||||
}
|
|
||||||
|
|
||||||
return ClaudeCodeValidator.validate(mockReq)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚀 转发请求到Claude API
|
// 🚀 转发请求到Claude API
|
||||||
@@ -151,8 +142,7 @@ class ClaudeRelayService {
|
|||||||
// 获取有效的访问token
|
// 获取有效的访问token
|
||||||
const accessToken = await claudeAccountService.getValidAccessToken(accountId)
|
const accessToken = await claudeAccountService.getValidAccessToken(accountId)
|
||||||
|
|
||||||
// 处理请求体(传递 clientHeaders 以判断是否需要设置 Claude Code 系统提示词)
|
const processedBody = this._processRequestBody(requestBody, account)
|
||||||
const processedBody = this._processRequestBody(requestBody, clientHeaders, account)
|
|
||||||
|
|
||||||
// 获取代理配置
|
// 获取代理配置
|
||||||
const proxyAgent = await this._getProxyAgent(accountId)
|
const proxyAgent = await this._getProxyAgent(accountId)
|
||||||
@@ -397,7 +387,7 @@ class ClaudeRelayService {
|
|||||||
if (
|
if (
|
||||||
clientHeaders &&
|
clientHeaders &&
|
||||||
Object.keys(clientHeaders).length > 0 &&
|
Object.keys(clientHeaders).length > 0 &&
|
||||||
this.isRealClaudeCodeRequest(requestBody, clientHeaders)
|
this.isRealClaudeCodeRequest(requestBody)
|
||||||
) {
|
) {
|
||||||
await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders)
|
await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders)
|
||||||
}
|
}
|
||||||
@@ -444,7 +434,7 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔄 处理请求体
|
// 🔄 处理请求体
|
||||||
_processRequestBody(body, clientHeaders = {}, account = null) {
|
_processRequestBody(body, account = null) {
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
@@ -459,7 +449,7 @@ class ClaudeRelayService {
|
|||||||
this._stripTtlFromCacheControl(processedBody)
|
this._stripTtlFromCacheControl(processedBody)
|
||||||
|
|
||||||
// 判断是否是真实的 Claude Code 请求
|
// 判断是否是真实的 Claude Code 请求
|
||||||
const isRealClaudeCode = this.isRealClaudeCodeRequest(processedBody, clientHeaders)
|
const isRealClaudeCode = this.isRealClaudeCodeRequest(processedBody)
|
||||||
|
|
||||||
// 如果不是真实的 Claude Code 请求,需要设置 Claude Code 系统提示词
|
// 如果不是真实的 Claude Code 请求,需要设置 Claude Code 系统提示词
|
||||||
if (!isRealClaudeCode) {
|
if (!isRealClaudeCode) {
|
||||||
@@ -760,7 +750,7 @@ class ClaudeRelayService {
|
|||||||
const filteredHeaders = this._filterClientHeaders(clientHeaders)
|
const filteredHeaders = this._filterClientHeaders(clientHeaders)
|
||||||
|
|
||||||
// 判断是否是真实的 Claude Code 请求
|
// 判断是否是真实的 Claude Code 请求
|
||||||
const isRealClaudeCode = this.isRealClaudeCodeRequest(body, clientHeaders)
|
const isRealClaudeCode = this.isRealClaudeCodeRequest(body)
|
||||||
|
|
||||||
// 如果不是真实的 Claude Code 请求,需要使用从账户获取的 Claude Code headers
|
// 如果不是真实的 Claude Code 请求,需要使用从账户获取的 Claude Code headers
|
||||||
const finalHeaders = { ...filteredHeaders }
|
const finalHeaders = { ...filteredHeaders }
|
||||||
@@ -1007,8 +997,7 @@ class ClaudeRelayService {
|
|||||||
// 获取有效的访问token
|
// 获取有效的访问token
|
||||||
const accessToken = await claudeAccountService.getValidAccessToken(accountId)
|
const accessToken = await claudeAccountService.getValidAccessToken(accountId)
|
||||||
|
|
||||||
// 处理请求体(传递 clientHeaders 以判断是否需要设置 Claude Code 系统提示词)
|
const processedBody = this._processRequestBody(requestBody, account)
|
||||||
const processedBody = this._processRequestBody(requestBody, clientHeaders, account)
|
|
||||||
|
|
||||||
// 获取代理配置
|
// 获取代理配置
|
||||||
const proxyAgent = await this._getProxyAgent(accountId)
|
const proxyAgent = await this._getProxyAgent(accountId)
|
||||||
@@ -1065,7 +1054,7 @@ class ClaudeRelayService {
|
|||||||
const filteredHeaders = this._filterClientHeaders(clientHeaders)
|
const filteredHeaders = this._filterClientHeaders(clientHeaders)
|
||||||
|
|
||||||
// 判断是否是真实的 Claude Code 请求
|
// 判断是否是真实的 Claude Code 请求
|
||||||
const isRealClaudeCode = this.isRealClaudeCodeRequest(body, clientHeaders)
|
const isRealClaudeCode = this.isRealClaudeCodeRequest(body)
|
||||||
|
|
||||||
// 如果不是真实的 Claude Code 请求,需要使用从账户获取的 Claude Code headers
|
// 如果不是真实的 Claude Code 请求,需要使用从账户获取的 Claude Code headers
|
||||||
const finalHeaders = { ...filteredHeaders }
|
const finalHeaders = { ...filteredHeaders }
|
||||||
@@ -1595,7 +1584,7 @@ class ClaudeRelayService {
|
|||||||
if (
|
if (
|
||||||
clientHeaders &&
|
clientHeaders &&
|
||||||
Object.keys(clientHeaders).length > 0 &&
|
Object.keys(clientHeaders).length > 0 &&
|
||||||
this.isRealClaudeCodeRequest(body, clientHeaders)
|
this.isRealClaudeCodeRequest(body)
|
||||||
) {
|
) {
|
||||||
await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders)
|
await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ const PROMPT_DEFINITIONS = {
|
|||||||
title: 'Claude Agent SDK System Prompt',
|
title: 'Claude Agent SDK System Prompt',
|
||||||
text: "You are a Claude agent, built on Anthropic's Claude Agent SDK."
|
text: "You are a Claude agent, built on Anthropic's Claude Agent SDK."
|
||||||
},
|
},
|
||||||
|
claudeOtherSystemPrompt4: {
|
||||||
|
category: 'system',
|
||||||
|
title: 'Claude Code Compact System Prompt Agent SDK2',
|
||||||
|
text: "You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK."
|
||||||
|
},
|
||||||
claudeOtherSystemPromptCompact: {
|
claudeOtherSystemPromptCompact: {
|
||||||
category: 'system',
|
category: 'system',
|
||||||
title: 'Claude Code Compact System Prompt',
|
title: 'Claude Code Compact System Prompt',
|
||||||
|
|||||||
71
src/utils/rateLimitHelper.js
Normal file
71
src/utils/rateLimitHelper.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
const redis = require('../models/redis')
|
||||||
|
const pricingService = require('../services/pricingService')
|
||||||
|
const CostCalculator = require('./costCalculator')
|
||||||
|
|
||||||
|
function toNumber(value) {
|
||||||
|
const num = Number(value)
|
||||||
|
return Number.isFinite(num) ? num : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRateLimitCounters(rateLimitInfo, usageSummary, model) {
|
||||||
|
if (!rateLimitInfo) {
|
||||||
|
return { totalTokens: 0, totalCost: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = redis.getClient()
|
||||||
|
if (!client) {
|
||||||
|
throw new Error('Redis 未连接,无法更新限流计数')
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputTokens = toNumber(usageSummary.inputTokens)
|
||||||
|
const outputTokens = toNumber(usageSummary.outputTokens)
|
||||||
|
const cacheCreateTokens = toNumber(usageSummary.cacheCreateTokens)
|
||||||
|
const cacheReadTokens = toNumber(usageSummary.cacheReadTokens)
|
||||||
|
|
||||||
|
const totalTokens = inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
|
||||||
|
|
||||||
|
if (totalTokens > 0 && rateLimitInfo.tokenCountKey) {
|
||||||
|
await client.incrby(rateLimitInfo.tokenCountKey, Math.round(totalTokens))
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalCost = 0
|
||||||
|
const usagePayload = {
|
||||||
|
input_tokens: inputTokens,
|
||||||
|
output_tokens: outputTokens,
|
||||||
|
cache_creation_input_tokens: cacheCreateTokens,
|
||||||
|
cache_read_input_tokens: cacheReadTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const costInfo = pricingService.calculateCost(usagePayload, model)
|
||||||
|
const { totalCost: calculatedCost } = costInfo || {}
|
||||||
|
if (typeof calculatedCost === 'number') {
|
||||||
|
totalCost = calculatedCost
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略此处错误,后续使用备用计算
|
||||||
|
totalCost = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalCost === 0) {
|
||||||
|
try {
|
||||||
|
const fallback = CostCalculator.calculateCost(usagePayload, model)
|
||||||
|
const { costs } = fallback || {}
|
||||||
|
if (costs && typeof costs.total === 'number') {
|
||||||
|
totalCost = costs.total
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
totalCost = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalCost > 0 && rateLimitInfo.costCountKey) {
|
||||||
|
await client.incrbyfloat(rateLimitInfo.costCountKey, totalCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { totalTokens, totalCost }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
updateRateLimitCounters
|
||||||
|
}
|
||||||
@@ -74,16 +74,7 @@ class ClaudeCodeValidator {
|
|||||||
const userAgent = req.headers['user-agent'] || ''
|
const userAgent = req.headers['user-agent'] || ''
|
||||||
const path = req.path || ''
|
const path = req.path || ''
|
||||||
|
|
||||||
// 1. 先检查是否是 Claude Code 的 User-Agent
|
const claudeCodePattern = /^claude-cli\/\d+\.\d+\.\d+/i;
|
||||||
// 支持的格式:
|
|
||||||
// - claude-cli/1.0.86 (external, cli) - 原有 CLI 格式
|
|
||||||
// - claude-cli/2.0.0 (external, claude-vscode) - VSCode 插件格式
|
|
||||||
// - claude-cli/x.x.x (external, sdk-py) - Python SDK 格式
|
|
||||||
// - claude-cli/x.x.x (external, sdk-js) - JavaScript SDK 格式
|
|
||||||
// - 其他 (external, claude-xxx) 或 (external, sdk-xxx) 格式
|
|
||||||
|
|
||||||
const claudeCodePattern =
|
|
||||||
/^claude-cli\/[\d.]+(?:[-\w]*)?\s+\(external,\s*(?:cli|claude-[\w-]+|sdk-[\w-]+)\)$/i
|
|
||||||
|
|
||||||
if (!claudeCodePattern.test(userAgent)) {
|
if (!claudeCodePattern.test(userAgent)) {
|
||||||
// 不是 Claude Code 的请求,此验证器不处理
|
// 不是 Claude Code 的请求,此验证器不处理
|
||||||
|
|||||||
@@ -38,6 +38,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 sm:text-sm">
|
<p class="text-xs text-gray-500 dark:text-gray-400 sm:text-sm">
|
||||||
近 {{ summary?.days || 30 }} 天内的费用与请求趋势
|
近 {{ summary?.days || 30 }} 天内的费用与请求趋势
|
||||||
|
<span v-if="summary?.actualDaysUsed && summary?.actualDaysUsed < summary?.days">
|
||||||
|
(日均基于实际使用 {{ summary.actualDaysUsed }} 天)
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -443,7 +446,10 @@ const primaryMetrics = computed(() => [
|
|||||||
key: 'avgCost',
|
key: 'avgCost',
|
||||||
label: '日均费用',
|
label: '日均费用',
|
||||||
value: props.summary?.avgDailyCostFormatted || formatCost(props.summary?.avgDailyCost || 0),
|
value: props.summary?.avgDailyCostFormatted || formatCost(props.summary?.avgDailyCost || 0),
|
||||||
subtitle: '平均每日成本',
|
subtitle:
|
||||||
|
props.summary?.actualDaysUsed && props.summary?.actualDaysUsed < props.summary?.days
|
||||||
|
? `基于 ${props.summary.actualDaysUsed} 天实际使用`
|
||||||
|
: '平均每日成本',
|
||||||
icon: 'fa-wave-square',
|
icon: 'fa-wave-square',
|
||||||
iconClass: 'text-purple-500'
|
iconClass: 'text-purple-500'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
|
import { API_PREFIX } from '@/config/api'
|
||||||
|
|
||||||
const API_BASE = '/users'
|
const API_BASE = `${API_PREFIX}/users`
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', {
|
export const useUserStore = defineStore('user', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user