feat: 实现基于费用的速率限制功能

- 新增 rateLimitCost 字段,支持按费用进行速率限制
- 新增 weeklyOpusCostLimit 字段,支持 Opus 模型周费用限制
- 优化速率限制逻辑,支持费用、请求数、token多维度控制
- 更新前端界面,添加费用限制配置选项
- 增强账户管理功能,支持费用统计和限制
- 改进 Redis 数据模型,支持费用计数器
- 优化价格计算服务,支持更精确的成本核算

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-08-31 17:27:37 +08:00
parent a54622e3d7
commit e84c6a5555
21 changed files with 1662 additions and 161 deletions

View File

@@ -180,15 +180,15 @@ class ClaudeRelayService {
// 记录401错误
await this.recordUnauthorizedError(accountId)
// 检查是否需要标记为异常(连续3次401
// 检查是否需要标记为异常(遇到1次401就停止调度
const errorCount = await this.getUnauthorizedErrorCount(accountId)
logger.info(
`🔐 Account ${accountId} has ${errorCount} consecutive 401 errors in the last 5 minutes`
)
if (errorCount >= 3) {
if (errorCount >= 1) {
logger.error(
`❌ Account ${accountId} exceeded 401 error threshold (${errorCount} errors), marking as unauthorized`
`❌ Account ${accountId} encountered 401 error (${errorCount} errors), marking as unauthorized`
)
await unifiedClaudeScheduler.markAccountUnauthorized(
accountId,
@@ -264,6 +264,27 @@ class ClaudeRelayService {
)
}
} else if (response.statusCode === 200 || response.statusCode === 201) {
// 提取5小时会话窗口状态
// 使用大小写不敏感的方式获取响应头
const get5hStatus = (headers) => {
if (!headers) {
return null
}
// HTTP头部名称不区分大小写需要处理不同情况
return (
headers['anthropic-ratelimit-unified-5h-status'] ||
headers['Anthropic-Ratelimit-Unified-5h-Status'] ||
headers['ANTHROPIC-RATELIMIT-UNIFIED-5H-STATUS']
)
}
const sessionWindowStatus = get5hStatus(response.headers)
if (sessionWindowStatus) {
logger.info(`📊 Session window status for account ${accountId}: ${sessionWindowStatus}`)
// 保存会话窗口状态到账户数据
await claudeAccountService.updateSessionWindowStatus(accountId, sessionWindowStatus)
}
// 请求成功清除401和500错误计数
await this.clearUnauthorizedErrors(accountId)
await claudeAccountService.clearInternalErrors(accountId)
@@ -454,7 +475,10 @@ class ClaudeRelayService {
const modelConfig = pricingData[model]
if (!modelConfig) {
logger.debug(`🔍 Model ${model} not found in pricing file, skipping max_tokens validation`)
// 如果找不到模型配置,直接透传客户端参数,不进行任何干预
logger.info(
`📝 Model ${model} not found in pricing file, passing through client parameters without modification`
)
return
}
@@ -1189,6 +1213,27 @@ class ClaudeRelayService {
usageCallback(finalUsage)
}
// 提取5小时会话窗口状态
// 使用大小写不敏感的方式获取响应头
const get5hStatus = (headers) => {
if (!headers) {
return null
}
// HTTP头部名称不区分大小写需要处理不同情况
return (
headers['anthropic-ratelimit-unified-5h-status'] ||
headers['Anthropic-Ratelimit-Unified-5h-Status'] ||
headers['ANTHROPIC-RATELIMIT-UNIFIED-5H-STATUS']
)
}
const sessionWindowStatus = get5hStatus(res.headers)
if (sessionWindowStatus) {
logger.info(`📊 Session window status for account ${accountId}: ${sessionWindowStatus}`)
// 保存会话窗口状态到账户数据
await claudeAccountService.updateSessionWindowStatus(accountId, sessionWindowStatus)
}
// 处理限流状态
if (rateLimitDetected || res.statusCode === 429) {
// 提取限流重置时间戳