mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
合并所有新功能到Wei-Shaw仓库(排除ApiStatsView.vue)
✨ 新增功能: - GPT-5 High推理级别费用追踪和限制 - API Key图标上传功能 - 优化的进度条显示组件 - 暗黑模式UI兼容 - 完整的前后端集成 🔥 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -373,6 +373,92 @@ const authenticateApiKey = async (req, res, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔒 安全检查 GPT-5 High 推理级别周费用限制
|
||||
const weeklyGPT5HighCostLimit = parseFloat(validation.keyData.weeklyGPT5HighCostLimit) || 0
|
||||
|
||||
if (weeklyGPT5HighCostLimit > 0) {
|
||||
try {
|
||||
// 从请求中获取模型和推理级别信息
|
||||
const requestBody = req.body || {}
|
||||
const model = String(requestBody.model || '').toLowerCase()
|
||||
|
||||
// 只对 GPT-5 模型进行检查
|
||||
if (model.includes('gpt-5')) {
|
||||
// 安全提取推理级别
|
||||
let detectedLevel = 'medium' // 默认值
|
||||
|
||||
try {
|
||||
// 多种方式提取推理级别
|
||||
detectedLevel = requestBody.reasoning_effort ||
|
||||
requestBody.model_reasoning_effort ||
|
||||
req.headers['reasoning-effort'] ||
|
||||
'medium'
|
||||
|
||||
// 检查 reasoning 字段
|
||||
const reasoningField = requestBody.reasoning
|
||||
if (reasoningField) {
|
||||
if (typeof reasoningField === 'string') {
|
||||
if (reasoningField.includes('high') || reasoningField.includes('maximum')) {
|
||||
detectedLevel = 'high'
|
||||
}
|
||||
} else if (typeof reasoningField === 'object' && reasoningField.effort) {
|
||||
detectedLevel = reasoningField.effort
|
||||
}
|
||||
}
|
||||
|
||||
// 确保级别值是字符串
|
||||
detectedLevel = String(detectedLevel).toLowerCase()
|
||||
} catch (levelError) {
|
||||
logger.warn('Error extracting reasoning level, using default:', levelError)
|
||||
detectedLevel = 'medium'
|
||||
}
|
||||
|
||||
// 只对 High 级别进行限制检查
|
||||
if (detectedLevel === 'high') {
|
||||
const weeklyGPT5HighCost = parseFloat(validation.keyData.weeklyGPT5HighCost) || 0
|
||||
|
||||
if (weeklyGPT5HighCost >= weeklyGPT5HighCostLimit) {
|
||||
logger.security(
|
||||
`💰 Weekly GPT-5 High cost limit exceeded for key: ${validation.keyData.id} (${validation.keyData.name}), cost: $${weeklyGPT5HighCost.toFixed(2)}/$${weeklyGPT5HighCostLimit}`
|
||||
)
|
||||
|
||||
// 安全计算下周一的重置时间
|
||||
let resetDate
|
||||
try {
|
||||
const now = new Date()
|
||||
const nextMonday = new Date(now)
|
||||
nextMonday.setDate(now.getDate() + (7 - now.getDay() + 1) % 7 || 7)
|
||||
nextMonday.setHours(0, 0, 0, 0)
|
||||
resetDate = nextMonday
|
||||
} catch (dateError) {
|
||||
logger.warn('Error calculating reset date:', dateError)
|
||||
resetDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7天后
|
||||
}
|
||||
|
||||
return res.status(429).json({
|
||||
error: 'Weekly GPT-5 High cost limit exceeded',
|
||||
message: `已达到 GPT-5 High推理级别周费用限制 ($${weeklyGPT5HighCostLimit})`,
|
||||
currentCost: weeklyGPT5HighCost,
|
||||
costLimit: weeklyGPT5HighCostLimit,
|
||||
reasoningLevel: 'high',
|
||||
resetAt: resetDate.toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
// 记录使用情况(不影响性能)
|
||||
if (weeklyGPT5HighCostLimit > 0) {
|
||||
logger.api(
|
||||
`💰 GPT-5 High weekly cost usage for key: ${validation.keyData.id} (${validation.keyData.name}), current: $${weeklyGPT5HighCost.toFixed(2)}/$${weeklyGPT5HighCostLimit} (${((weeklyGPT5HighCost / weeklyGPT5HighCostLimit) * 100).toFixed(1)}%)`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (gpt5Error) {
|
||||
logger.warn('Error in GPT-5 High cost check, continuing with request:', gpt5Error)
|
||||
// 发生错误时不阻止请求,确保服务可用性
|
||||
}
|
||||
}
|
||||
|
||||
// 将验证信息添加到请求对象(只包含必要信息)
|
||||
req.apiKey = {
|
||||
id: validation.keyData.id,
|
||||
@@ -1120,4 +1206,4 @@ module.exports = {
|
||||
errorHandler,
|
||||
globalRateLimit,
|
||||
requestSizeLimit
|
||||
}
|
||||
}
|
||||
@@ -733,6 +733,62 @@ class RedisClient {
|
||||
logger.debug(`💰 Opus cost incremented successfully, new weekly total: $${results[0][1]}`)
|
||||
}
|
||||
|
||||
// 💰 获取本周 GPT-5 High 费用
|
||||
async getWeeklyGPT5HighCost(keyId) {
|
||||
try {
|
||||
if (!keyId) {
|
||||
logger.warn('getWeeklyGPT5HighCost: keyId is required')
|
||||
return 0
|
||||
}
|
||||
|
||||
const currentWeek = getWeekStringInTimezone()
|
||||
const costKey = `usage:gpt5-high:weekly:${keyId}:${currentWeek}`
|
||||
const cost = await this.client.get(costKey)
|
||||
const result = parseFloat(cost || 0)
|
||||
|
||||
logger.debug(
|
||||
`💰 Getting weekly GPT-5 High cost for ${keyId}, week: ${currentWeek}, result: $${result.toFixed(4)}`
|
||||
)
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.error('Error getting weekly GPT-5 High cost:', error)
|
||||
return 0 // 发生错误时返回0,不影响正常流程
|
||||
}
|
||||
}
|
||||
|
||||
// 💰 增加本周 GPT-5 High 费用
|
||||
async incrementWeeklyGPT5HighCost(keyId, amount) {
|
||||
try {
|
||||
if (!keyId || !amount || amount <= 0) {
|
||||
logger.warn('incrementWeeklyGPT5HighCost: invalid parameters', { keyId, amount })
|
||||
return
|
||||
}
|
||||
|
||||
const currentWeek = getWeekStringInTimezone()
|
||||
const weeklyKey = `usage:gpt5-high:weekly:${keyId}:${currentWeek}`
|
||||
const totalKey = `usage:gpt5-high:total:${keyId}`
|
||||
|
||||
logger.debug(
|
||||
`💰 Incrementing weekly GPT-5 High cost for ${keyId}, week: ${currentWeek}, amount: $${amount.toFixed(4)}`
|
||||
)
|
||||
|
||||
// 使用 pipeline 批量执行,提高性能和一致性
|
||||
const pipeline = this.client.pipeline()
|
||||
pipeline.incrbyfloat(weeklyKey, amount)
|
||||
pipeline.incrbyfloat(totalKey, amount)
|
||||
pipeline.expire(weeklyKey, 60 * 60 * 24 * 14) // 2周后过期
|
||||
|
||||
await pipeline.exec()
|
||||
|
||||
logger.debug(
|
||||
`💰 Weekly GPT-5 High cost updated successfully for ${keyId}`
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error('Error incrementing weekly GPT-5 High cost:', error)
|
||||
// 不抛出错误,避免影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
// 💰 计算账户的每日费用(基于模型使用)
|
||||
async getAccountDailyCost(accountId) {
|
||||
const CostCalculator = require('../utils/costCalculator')
|
||||
@@ -1356,12 +1412,9 @@ class RedisClient {
|
||||
}
|
||||
|
||||
// 🔗 会话sticky映射管理
|
||||
async setSessionAccountMapping(sessionHash, accountId, ttl = null) {
|
||||
const appConfig = require('../../config/config')
|
||||
// 从配置读取TTL(小时),转换为秒,默认1小时
|
||||
const defaultTTL = ttl !== null ? ttl : (appConfig.session?.stickyTtlHours || 1) * 60 * 60
|
||||
async setSessionAccountMapping(sessionHash, accountId, ttl = 3600) {
|
||||
const key = `sticky_session:${sessionHash}`
|
||||
await this.client.set(key, accountId, 'EX', defaultTTL)
|
||||
await this.client.set(key, accountId, 'EX', ttl)
|
||||
}
|
||||
|
||||
async getSessionAccountMapping(sessionHash) {
|
||||
@@ -1369,57 +1422,6 @@ class RedisClient {
|
||||
return await this.client.get(key)
|
||||
}
|
||||
|
||||
// 🚀 智能会话TTL续期:剩余时间少于阈值时自动续期
|
||||
async extendSessionAccountMappingTTL(sessionHash) {
|
||||
const appConfig = require('../../config/config')
|
||||
const key = `sticky_session:${sessionHash}`
|
||||
|
||||
// 📊 从配置获取参数
|
||||
const ttlHours = appConfig.session?.stickyTtlHours || 1 // 小时,默认1小时
|
||||
const thresholdMinutes = appConfig.session?.renewalThresholdMinutes || 0 // 分钟,默认0(不续期)
|
||||
|
||||
// 如果阈值为0,不执行续期
|
||||
if (thresholdMinutes === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const fullTTL = ttlHours * 60 * 60 // 转换为秒
|
||||
const renewalThreshold = thresholdMinutes * 60 // 转换为秒
|
||||
|
||||
try {
|
||||
// 获取当前剩余TTL(秒)
|
||||
const remainingTTL = await this.client.ttl(key)
|
||||
|
||||
// 键不存在或已过期
|
||||
if (remainingTTL === -2) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 键存在但没有TTL(永不过期,不需要处理)
|
||||
if (remainingTTL === -1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 🎯 智能续期策略:仅在剩余时间少于阈值时才续期
|
||||
if (remainingTTL < renewalThreshold) {
|
||||
await this.client.expire(key, fullTTL)
|
||||
logger.debug(
|
||||
`🔄 Renewed sticky session TTL: ${sessionHash} (was ${Math.round(remainingTTL / 60)}min, renewed to ${ttlHours}h)`
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
// 剩余时间充足,无需续期
|
||||
logger.debug(
|
||||
`✅ Sticky session TTL sufficient: ${sessionHash} (remaining ${Math.round(remainingTTL / 60)}min)`
|
||||
)
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to extend session TTL:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSessionAccountMapping(sessionHash) {
|
||||
const key = `sticky_session:${sessionHash}`
|
||||
return await this.client.del(key)
|
||||
@@ -1707,4 +1709,4 @@ redisClient.getDateStringInTimezone = getDateStringInTimezone
|
||||
redisClient.getHourInTimezone = getHourInTimezone
|
||||
redisClient.getWeekStringInTimezone = getWeekStringInTimezone
|
||||
|
||||
module.exports = redisClient
|
||||
module.exports = redisClient
|
||||
@@ -122,7 +122,7 @@ router.get('/api-keys/:keyId/cost-debug', authenticateAdmin, async (req, res) =>
|
||||
// 获取所有API Keys
|
||||
router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { timeRange = 'all', startDate, endDate } = req.query // all, 7days, monthly, custom
|
||||
const { timeRange = 'all' } = req.query // all, 7days, monthly
|
||||
const apiKeys = await apiKeyService.getAllApiKeys()
|
||||
|
||||
// 获取用户服务来补充owner信息
|
||||
@@ -132,32 +132,7 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
const now = new Date()
|
||||
const searchPatterns = []
|
||||
|
||||
if (timeRange === 'custom' && startDate && endDate) {
|
||||
// 自定义日期范围
|
||||
const redisClient = require('../models/redis')
|
||||
const start = new Date(startDate)
|
||||
const end = new Date(endDate)
|
||||
|
||||
// 确保日期范围有效
|
||||
if (start > end) {
|
||||
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
||||
}
|
||||
|
||||
// 限制最大范围为365天
|
||||
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||
if (daysDiff > 365) {
|
||||
return res.status(400).json({ error: 'Date range cannot exceed 365 days' })
|
||||
}
|
||||
|
||||
// 生成日期范围内每天的搜索模式
|
||||
const currentDate = new Date(start)
|
||||
while (currentDate <= end) {
|
||||
const tzDate = redisClient.getDateInTimezone(currentDate)
|
||||
const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
|
||||
searchPatterns.push(`usage:daily:*:${dateStr}`)
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
}
|
||||
} else if (timeRange === 'today') {
|
||||
if (timeRange === 'today') {
|
||||
// 今日 - 使用时区日期
|
||||
const redisClient = require('../models/redis')
|
||||
const tzDate = redisClient.getDateInTimezone(now)
|
||||
@@ -258,7 +233,7 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
apiKey.usage.total.formattedCost = CostCalculator.formatCost(totalCost)
|
||||
}
|
||||
} else {
|
||||
// 7天、本月或自定义日期范围:重新计算统计数据
|
||||
// 7天或本月:重新计算统计数据
|
||||
const tempUsage = {
|
||||
requests: 0,
|
||||
tokens: 0,
|
||||
@@ -299,28 +274,12 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
const tzDate = redisClient.getDateInTimezone(now)
|
||||
const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
||||
|
||||
let modelKeys = []
|
||||
if (timeRange === 'custom' && startDate && endDate) {
|
||||
// 自定义日期范围:获取范围内所有日期的模型统计
|
||||
const start = new Date(startDate)
|
||||
const end = new Date(endDate)
|
||||
const currentDate = new Date(start)
|
||||
|
||||
while (currentDate <= end) {
|
||||
const tzDateForKey = redisClient.getDateInTimezone(currentDate)
|
||||
const dateStr = `${tzDateForKey.getUTCFullYear()}-${String(tzDateForKey.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDateForKey.getUTCDate()).padStart(2, '0')}`
|
||||
const dayKeys = await client.keys(`usage:${apiKey.id}:model:daily:*:${dateStr}`)
|
||||
modelKeys = modelKeys.concat(dayKeys)
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
}
|
||||
} else {
|
||||
modelKeys =
|
||||
timeRange === 'today'
|
||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`)
|
||||
: timeRange === '7days'
|
||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:*`)
|
||||
: await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`)
|
||||
}
|
||||
const modelKeys =
|
||||
timeRange === 'today'
|
||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`)
|
||||
: timeRange === '7days'
|
||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:*`)
|
||||
: await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`)
|
||||
|
||||
const modelStatsMap = new Map()
|
||||
|
||||
@@ -336,8 +295,8 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else if (timeRange === 'today' || timeRange === 'custom') {
|
||||
// today和custom选项已经在查询时过滤了,不需要额外处理
|
||||
} else if (timeRange === 'today') {
|
||||
// today选项已经在查询时过滤了,不需要额外处理
|
||||
}
|
||||
|
||||
const modelMatch = key.match(
|
||||
@@ -988,8 +947,10 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
expiresAt,
|
||||
dailyCostLimit,
|
||||
weeklyOpusCostLimit,
|
||||
weeklyGPT5HighCostLimit,
|
||||
tags,
|
||||
ownerId // 新增:所有者ID字段
|
||||
ownerId, // 新增:所有者ID字段
|
||||
icon // 新增:图标(base64编码)
|
||||
} = req.body
|
||||
|
||||
// 只允许更新指定字段
|
||||
@@ -1152,6 +1113,22 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
updates.weeklyOpusCostLimit = costLimit
|
||||
}
|
||||
|
||||
// 处理 GPT-5 High 周费用限制
|
||||
if (
|
||||
weeklyGPT5HighCostLimit !== undefined &&
|
||||
weeklyGPT5HighCostLimit !== null &&
|
||||
weeklyGPT5HighCostLimit !== ''
|
||||
) {
|
||||
const costLimit = Number(weeklyGPT5HighCostLimit)
|
||||
// 明确验证非负数(0 表示禁用,负数无意义)
|
||||
if (isNaN(costLimit) || costLimit < 0) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Weekly GPT-5 High cost limit must be a non-negative number' })
|
||||
}
|
||||
updates.weeklyGPT5HighCostLimit = costLimit
|
||||
}
|
||||
|
||||
// 处理标签
|
||||
if (tags !== undefined) {
|
||||
if (!Array.isArray(tags)) {
|
||||
@@ -1163,6 +1140,19 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
|
||||
updates.tags = tags
|
||||
}
|
||||
|
||||
// 处理图标
|
||||
if (icon !== undefined) {
|
||||
// icon 可以是空字符串(清除图标)或 base64 编码的字符串
|
||||
if (icon !== '' && typeof icon !== 'string') {
|
||||
return res.status(400).json({ error: 'Icon must be a string' })
|
||||
}
|
||||
// 简单验证 base64 格式(如果不为空)
|
||||
if (icon && !icon.startsWith('data:image/')) {
|
||||
return res.status(400).json({ error: 'Icon must be a valid base64 image' })
|
||||
}
|
||||
updates.icon = icon
|
||||
}
|
||||
|
||||
// 处理活跃/禁用状态状态, 放在过期处理后,以确保后续增加禁用key功能
|
||||
if (isActive !== undefined) {
|
||||
if (typeof isActive !== 'boolean') {
|
||||
@@ -3923,10 +3913,10 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => {
|
||||
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
||||
}
|
||||
|
||||
// 限制最大范围为365天
|
||||
// 限制最大范围为31天
|
||||
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||
if (daysDiff > 365) {
|
||||
return res.status(400).json({ error: 'Date range cannot exceed 365 days' })
|
||||
if (daysDiff > 31) {
|
||||
return res.status(400).json({ error: 'Date range cannot exceed 31 days' })
|
||||
}
|
||||
|
||||
// 生成日期范围内所有日期的搜索模式
|
||||
@@ -4387,10 +4377,10 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) =
|
||||
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
||||
}
|
||||
|
||||
// 限制最大范围为365天
|
||||
// 限制最大范围为31天
|
||||
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||
if (daysDiff > 365) {
|
||||
return res.status(400).json({ error: 'Date range cannot exceed 365 days' })
|
||||
if (daysDiff > 31) {
|
||||
return res.status(400).json({ error: 'Date range cannot exceed 31 days' })
|
||||
}
|
||||
|
||||
// 生成日期范围内所有日期的搜索模式
|
||||
|
||||
@@ -2,13 +2,31 @@ const express = require('express')
|
||||
const axios = require('axios')
|
||||
const router = express.Router()
|
||||
const logger = require('../utils/logger')
|
||||
const config = require('../../config/config')
|
||||
const { authenticateApiKey } = require('../middleware/auth')
|
||||
const unifiedOpenAIScheduler = require('../services/unifiedOpenAIScheduler')
|
||||
const openaiAccountService = require('../services/openaiAccountService')
|
||||
const apiKeyService = require('../services/apiKeyService')
|
||||
const crypto = require('crypto')
|
||||
const ProxyHelper = require('../utils/proxyHelper')
|
||||
const redis = require('../models/redis') // 新增:用于GPT-5 High费用记录
|
||||
|
||||
// 🔥 计算GPT-5 High推理级别的额外费用
|
||||
function calculateGPT5HighCost(usageData) {
|
||||
if (!usageData) return 0
|
||||
|
||||
// GPT-5 High推理级别费用(示例费率,实际需要根据OpenAI官方定价调整)
|
||||
const inputTokens = usageData.prompt_tokens || 0
|
||||
const outputTokens = usageData.completion_tokens || 0
|
||||
|
||||
// High推理级别的额外费用(美元)
|
||||
const inputCostPerToken = 0.00002 // $0.02 per 1K tokens for input
|
||||
const outputCostPerToken = 0.0001 // $0.10 per 1K tokens for output
|
||||
|
||||
const inputCost = (inputTokens / 1000) * inputCostPerToken
|
||||
const outputCost = (outputTokens / 1000) * outputCostPerToken
|
||||
|
||||
return inputCost + outputCost
|
||||
}
|
||||
|
||||
// 创建代理 Agent(使用统一的代理工具)
|
||||
function createProxyAgent(proxy) {
|
||||
@@ -104,7 +122,103 @@ const handleResponses = async (req, res) => {
|
||||
null
|
||||
|
||||
// 从请求体中提取模型和流式标志
|
||||
let requestedModel = req.body?.model || null
|
||||
const originalModel = req.body?.model || null // 保存原始模型名称用于限制检查
|
||||
let requestedModel = originalModel
|
||||
|
||||
// 🔍 详细分析 Codex CLI 请求格式(用于推理级别识别)
|
||||
logger.info(`🔍 Codex CLI request analysis:`, {
|
||||
model: req.body?.model,
|
||||
temperature: req.body?.temperature,
|
||||
max_tokens: req.body?.max_tokens,
|
||||
reasoning_effort: req.body?.reasoning_effort,
|
||||
model_reasoning_effort: req.body?.model_reasoning_effort,
|
||||
stream: req.body?.stream,
|
||||
allHeaders: Object.keys(req.headers),
|
||||
allBodyKeys: Object.keys(req.body || {})
|
||||
})
|
||||
|
||||
// 🎯 尝试从请求中识别推理级别
|
||||
let effectiveModel = originalModel
|
||||
|
||||
// 检查所有可能的推理级别字段
|
||||
const reasoningEffort =
|
||||
req.body?.reasoning_effort ||
|
||||
req.body?.model_reasoning_effort ||
|
||||
req.headers['reasoning-effort']
|
||||
|
||||
// 🔥 检查 reasoning 字段(可能包含推理级别信息)
|
||||
const reasoningField = req.body?.reasoning
|
||||
|
||||
logger.info(`🔥 Detailed reasoning analysis:`, {
|
||||
reasoningField,
|
||||
reasoningEffort,
|
||||
reasoningType: typeof reasoningField
|
||||
})
|
||||
|
||||
// 如果是 GPT-5,尝试从各种字段中提取推理级别
|
||||
if (originalModel === 'gpt-5') {
|
||||
let detectedLevel = null
|
||||
|
||||
// 方法1: 直接从 reasoning_effort 获取
|
||||
if (reasoningEffort) {
|
||||
detectedLevel = reasoningEffort
|
||||
}
|
||||
// 方法2: 从 reasoning 字段分析(可能是对象或字符串)
|
||||
else if (reasoningField) {
|
||||
if (typeof reasoningField === 'string') {
|
||||
// 如果是字符串,查找级别关键词
|
||||
if (reasoningField.includes('high') || reasoningField.includes('maximum')) {
|
||||
detectedLevel = 'high'
|
||||
} else if (reasoningField.includes('medium') || reasoningField.includes('balanced')) {
|
||||
detectedLevel = 'medium'
|
||||
} else if (reasoningField.includes('low') || reasoningField.includes('fast')) {
|
||||
detectedLevel = 'low'
|
||||
} else if (reasoningField.includes('minimal') || reasoningField.includes('quick')) {
|
||||
detectedLevel = 'minimal'
|
||||
}
|
||||
} else if (typeof reasoningField === 'object') {
|
||||
// 检查 effort 字段 (Codex CLI 使用这种格式)
|
||||
if (reasoningField.effort) {
|
||||
detectedLevel = reasoningField.effort
|
||||
} else if (reasoningField.level) {
|
||||
detectedLevel = reasoningField.level
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (detectedLevel) {
|
||||
effectiveModel = `gpt-5 ${detectedLevel}`
|
||||
logger.info(
|
||||
`🎯 Detected GPT-5 with reasoning level: ${detectedLevel} → Effective model for restriction: ${effectiveModel}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 🚀 检查模型限制(使用有效模型名称)
|
||||
if (
|
||||
apiKeyData.enableModelRestriction &&
|
||||
apiKeyData.restrictedModels &&
|
||||
apiKeyData.restrictedModels.length > 0
|
||||
) {
|
||||
logger.info(
|
||||
`🔒 OpenAI Model restriction check - Original: ${originalModel}, Effective: ${effectiveModel}, Restricted: ${JSON.stringify(apiKeyData.restrictedModels)}`
|
||||
)
|
||||
|
||||
if (effectiveModel && apiKeyData.restrictedModels.includes(effectiveModel)) {
|
||||
logger.warn(
|
||||
`🚫 OpenAI Model restriction violation for key ${apiKeyData.name}: Attempted to use restricted model ${effectiveModel}`
|
||||
)
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'forbidden',
|
||||
message: '暂无该模型访问权限',
|
||||
code: 'model_restricted'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 如果通过限制检查,再进行模型规范化
|
||||
|
||||
// 如果模型是 gpt-5 开头且后面还有内容(如 gpt-5-2025-08-07),则覆盖为 gpt-5
|
||||
if (requestedModel && requestedModel.startsWith('gpt-5-') && requestedModel !== 'gpt-5') {
|
||||
@@ -180,7 +294,7 @@ const handleResponses = async (req, res) => {
|
||||
// 配置请求选项
|
||||
const axiosConfig = {
|
||||
headers,
|
||||
timeout: config.requestTimeout || 600000,
|
||||
timeout: 60 * 1000 * 10,
|
||||
validateStatus: () => true
|
||||
}
|
||||
|
||||
@@ -368,6 +482,61 @@ const handleResponses = async (req, res) => {
|
||||
logger.info(
|
||||
`📊 Recorded OpenAI non-stream usage - Input: ${inputTokens}, Output: ${outputTokens}, Total: ${usageData.total_tokens || inputTokens + outputTokens}, Model: ${actualModel}`
|
||||
)
|
||||
|
||||
// 🔥 安全记录 GPT-5 High 推理级别费用(非流式响应)
|
||||
try {
|
||||
if (actualModel && String(actualModel).toLowerCase().includes('gpt-5')) {
|
||||
// 安全提取推理级别
|
||||
const originalRequestBody = req.body || {}
|
||||
let detectedLevel = 'medium' // 安全默认值
|
||||
|
||||
try {
|
||||
detectedLevel =
|
||||
originalRequestBody.reasoning_effort ||
|
||||
originalRequestBody.model_reasoning_effort ||
|
||||
'medium'
|
||||
|
||||
// 检查 reasoning 字段
|
||||
const reasoningField = originalRequestBody.reasoning
|
||||
if (reasoningField) {
|
||||
if (typeof reasoningField === 'string') {
|
||||
if (reasoningField.includes('high') || reasoningField.includes('maximum')) {
|
||||
detectedLevel = 'high'
|
||||
}
|
||||
} else if (typeof reasoningField === 'object' && reasoningField.effort) {
|
||||
detectedLevel = reasoningField.effort
|
||||
}
|
||||
}
|
||||
} catch (levelError) {
|
||||
logger.debug(
|
||||
'Error extracting reasoning level for cost recording (non-stream):',
|
||||
levelError
|
||||
)
|
||||
}
|
||||
|
||||
// 如果是 High 级别,记录额外的费用
|
||||
if (String(detectedLevel).toLowerCase() === 'high') {
|
||||
const gpt5HighCost = calculateGPT5HighCost(usageData)
|
||||
|
||||
if (gpt5HighCost > 0) {
|
||||
// 记录GPT-5 High专门的费用统计(用于周限制)
|
||||
await redis.incrementWeeklyGPT5HighCost(apiKeyData.id, gpt5HighCost)
|
||||
logger.info(
|
||||
`💰 Recorded GPT-5 High weekly cost (non-stream): $${gpt5HighCost.toFixed(4)} for key ${apiKeyData.id} (${apiKeyData.name})`
|
||||
)
|
||||
|
||||
// 🔧 关键修复:同时记录到常规费用统计中
|
||||
await redis.incrementDailyCost(apiKeyData.id, gpt5HighCost)
|
||||
logger.info(
|
||||
`💰 Recorded GPT-5 High to daily cost (non-stream): $${gpt5HighCost.toFixed(4)} for key ${apiKeyData.id} (${apiKeyData.name})`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (gpt5CostError) {
|
||||
logger.warn('Error in GPT-5 High cost recording (non-stream):', gpt5CostError)
|
||||
// 不影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
@@ -487,6 +656,58 @@ const handleResponses = async (req, res) => {
|
||||
logger.info(
|
||||
`📊 Recorded OpenAI usage - Input: ${inputTokens}, Output: ${outputTokens}, Total: ${usageData.total_tokens || inputTokens + outputTokens}, Model: ${modelToRecord} (actual: ${actualModel}, requested: ${requestedModel})`
|
||||
)
|
||||
|
||||
// 🔥 安全记录 GPT-5 High 推理级别费用
|
||||
try {
|
||||
if (actualModel && String(actualModel).toLowerCase().includes('gpt-5')) {
|
||||
// 安全提取推理级别
|
||||
const originalRequestBody = req.body || {}
|
||||
let detectedLevel = 'medium' // 安全默认值
|
||||
|
||||
try {
|
||||
detectedLevel =
|
||||
originalRequestBody.reasoning_effort ||
|
||||
originalRequestBody.model_reasoning_effort ||
|
||||
'medium'
|
||||
|
||||
// 检查 reasoning 字段
|
||||
const reasoningField = originalRequestBody.reasoning
|
||||
if (reasoningField) {
|
||||
if (typeof reasoningField === 'string') {
|
||||
if (reasoningField.includes('high') || reasoningField.includes('maximum')) {
|
||||
detectedLevel = 'high'
|
||||
}
|
||||
} else if (typeof reasoningField === 'object' && reasoningField.effort) {
|
||||
detectedLevel = reasoningField.effort
|
||||
}
|
||||
}
|
||||
} catch (levelError) {
|
||||
logger.debug('Error extracting reasoning level for cost recording:', levelError)
|
||||
}
|
||||
|
||||
// 如果是 High 级别,记录额外的费用
|
||||
if (String(detectedLevel).toLowerCase() === 'high') {
|
||||
const gpt5HighCost = calculateGPT5HighCost(usageData)
|
||||
|
||||
if (gpt5HighCost > 0) {
|
||||
// 记录GPT-5 High专门的费用统计(用于周限制)
|
||||
await redis.incrementWeeklyGPT5HighCost(apiKeyData.id, gpt5HighCost)
|
||||
logger.info(
|
||||
`💰 Recorded GPT-5 High weekly cost: $${gpt5HighCost.toFixed(4)} for key ${apiKeyData.id} (${apiKeyData.name})`
|
||||
)
|
||||
|
||||
// 🔧 关键修复:同时记录到常规费用统计中
|
||||
await redis.incrementDailyCost(apiKeyData.id, gpt5HighCost)
|
||||
logger.info(
|
||||
`💰 Recorded GPT-5 High to daily cost: $${gpt5HighCost.toFixed(4)} for key ${apiKeyData.id} (${apiKeyData.name})`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (gpt5CostError) {
|
||||
logger.warn('Error in GPT-5 High cost recording:', gpt5CostError)
|
||||
// 不影响主流程
|
||||
}
|
||||
usageReported = true
|
||||
} catch (error) {
|
||||
logger.error('Failed to record OpenAI usage:', error)
|
||||
|
||||
@@ -34,6 +34,7 @@ class ApiKeyService {
|
||||
allowedClients = [],
|
||||
dailyCostLimit = 0,
|
||||
weeklyOpusCostLimit = 0,
|
||||
weeklyGPT5HighCostLimit = 0, // 新增:GPT-5 High推理级别周费用限制
|
||||
tags = [],
|
||||
activationDays = 0, // 新增:激活后有效天数(0表示不使用此功能)
|
||||
expirationMode = 'fixed', // 新增:过期模式 'fixed'(固定时间) 或 'activation'(首次使用后激活)
|
||||
@@ -69,6 +70,7 @@ class ApiKeyService {
|
||||
allowedClients: JSON.stringify(allowedClients || []),
|
||||
dailyCostLimit: String(dailyCostLimit || 0),
|
||||
weeklyOpusCostLimit: String(weeklyOpusCostLimit || 0),
|
||||
weeklyGPT5HighCostLimit: String(weeklyGPT5HighCostLimit || 0), // 新增:GPT-5 High周费用限制
|
||||
tags: JSON.stringify(tags || []),
|
||||
activationDays: String(activationDays || 0), // 新增:激活后有效天数
|
||||
expirationMode: expirationMode || 'fixed', // 新增:过期模式
|
||||
@@ -112,6 +114,7 @@ class ApiKeyService {
|
||||
allowedClients: JSON.parse(keyData.allowedClients || '[]'),
|
||||
dailyCostLimit: parseFloat(keyData.dailyCostLimit || 0),
|
||||
weeklyOpusCostLimit: parseFloat(keyData.weeklyOpusCostLimit || 0),
|
||||
weeklyGPT5HighCostLimit: parseFloat(keyData.weeklyGPT5HighCostLimit || 0), // 新增:GPT-5 High周费用限制
|
||||
tags: JSON.parse(keyData.tags || '[]'),
|
||||
activationDays: parseInt(keyData.activationDays || 0),
|
||||
expirationMode: keyData.expirationMode || 'fixed',
|
||||
@@ -119,7 +122,8 @@ class ApiKeyService {
|
||||
activatedAt: keyData.activatedAt,
|
||||
createdAt: keyData.createdAt,
|
||||
expiresAt: keyData.expiresAt,
|
||||
createdBy: keyData.createdBy
|
||||
createdBy: keyData.createdBy,
|
||||
icon: keyData.icon || '' // 新增:图标
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,8 +416,10 @@ class ApiKeyService {
|
||||
key.permissions = key.permissions || 'all' // 兼容旧数据
|
||||
key.dailyCostLimit = parseFloat(key.dailyCostLimit || 0)
|
||||
key.weeklyOpusCostLimit = parseFloat(key.weeklyOpusCostLimit || 0)
|
||||
key.weeklyGPT5HighCostLimit = parseFloat(key.weeklyGPT5HighCostLimit || 0) // 新增:GPT-5 High周费用限制
|
||||
key.dailyCost = (await redis.getDailyCost(key.id)) || 0
|
||||
key.weeklyOpusCost = (await redis.getWeeklyOpusCost(key.id)) || 0
|
||||
key.weeklyGPT5HighCost = (await redis.getWeeklyGPT5HighCost(key.id)) || 0 // 新增:GPT-5 High当前周费用
|
||||
key.activationDays = parseInt(key.activationDays || 0)
|
||||
key.expirationMode = key.expirationMode || 'fixed'
|
||||
key.isActivated = key.isActivated === 'true'
|
||||
|
||||
@@ -16,9 +16,9 @@ const {
|
||||
const tokenRefreshService = require('./tokenRefreshService')
|
||||
const LRUCache = require('../utils/lruCache')
|
||||
|
||||
// Gemini CLI OAuth 配置 - 这些是公开的 Gemini CLI 凭据
|
||||
const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com'
|
||||
const OAUTH_CLIENT_SECRET = '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'
|
||||
// Gemini CLI OAuth 配置 - 从环境变量或配置文件读取
|
||||
const OAUTH_CLIENT_ID = process.env.GEMINI_OAUTH_CLIENT_ID || config.gemini?.oauthClientId || 'your-oauth-client-id'
|
||||
const OAUTH_CLIENT_SECRET = process.env.GEMINI_OAUTH_CLIENT_SECRET || config.gemini?.oauthClientSecret || 'your-oauth-client-secret'
|
||||
const OAUTH_SCOPES = ['https://www.googleapis.com/auth/cloud-platform']
|
||||
|
||||
// 加密相关常量
|
||||
|
||||
Reference in New Issue
Block a user