Merge branch 'Wei-Shaw:dev' into dev

This commit is contained in:
sususu98
2025-09-10 14:22:45 +08:00
committed by GitHub
12 changed files with 775 additions and 805 deletions

View File

@@ -123,7 +123,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信息
@@ -133,32 +133,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)
@@ -259,7 +234,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,
@@ -300,28 +275,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()
@@ -337,8 +296,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(
@@ -989,8 +948,10 @@ router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => {
expiresAt,
dailyCostLimit,
weeklyOpusCostLimit,
weeklyGPT5HighCostLimit,
tags,
ownerId // 新增所有者ID字段
ownerId, // 新增所有者ID字段
icon // 新增图标base64编码
} = req.body
// 只允许更新指定字段
@@ -1153,6 +1114,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)) {
@@ -1164,6 +1141,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') {
@@ -4338,10 +4328,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' })
}
// 生成日期范围内所有日期的搜索模式
@@ -4802,10 +4792,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' })
}
// 生成日期范围内所有日期的搜索模式

View File

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