From cd72a296749513ea734891172ef68f3d1719713f Mon Sep 17 00:00:00 2001 From: shaw Date: Sat, 4 Oct 2025 11:10:55 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20opus=E5=91=A8=E9=99=90=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E5=A2=9E=E5=8A=A0=E9=87=8D=E7=BD=AE=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/claudeRelayService.js | 70 +++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index fc1b1404..5c7ac945 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -11,6 +11,7 @@ const config = require('../../config/config') const claudeCodeHeadersService = require('./claudeCodeHeadersService') const redis = require('../models/redis') const ClaudeCodeValidator = require('../validators/clients/claudeCodeValidator') +const { formatDateWithTimezone } = require('../utils/dateHelper') class ClaudeRelayService { constructor() { @@ -21,6 +22,14 @@ class ClaudeRelayService { this.claudeCodeSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude." } + _buildOpusLimitMessage(resetTime) { + if (!resetTime) { + return '此专属账号的Opus模型已达到周使用限制,请尝试切换其他模型后再试。' + } + const formattedReset = formatDateWithTimezone(resetTime) + return `此专属账号的Opus模型已达到周使用限制,将于 ${formattedReset} 自动恢复,请尝试切换其他模型后再试。` + } + // 🔍 判断是否是真实的 Claude Code 请求 isRealClaudeCodeRequest(requestBody, clientHeaders) { // 使用 claudeCodeValidator 来进行完整的验证 @@ -83,6 +92,7 @@ class ClaudeRelayService { } const isDedicatedOfficialAccount = + accountType === 'claude-official' && apiKeyData.claudeAccountId && !apiKeyData.claudeAccountId.startsWith('group:') && apiKeyData.claudeAccountId === accountId @@ -95,6 +105,7 @@ class ClaudeRelayService { } if (isOpusModelRequest && isDedicatedOfficialAccount && opusRateLimitActive) { + const limitMessage = this._buildOpusLimitMessage(opusRateLimitEndAt) logger.warn( `🚫 Dedicated account ${account?.name || accountId} is under Opus weekly limit until ${opusRateLimitEndAt}` ) @@ -103,7 +114,7 @@ class ClaudeRelayService { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'opus_weekly_limit', - message: '此专属账号的Opus模型已达到本周使用限制,请尝试切换其他模型后再试。' + message: limitMessage }), accountId } @@ -226,6 +237,19 @@ class ClaudeRelayService { logger.warn( `🚫 Account ${accountId} hit Opus limit, resets at ${new Date(parsedResetTimestamp * 1000).toISOString()}` ) + + if (isDedicatedOfficialAccount) { + const limitMessage = this._buildOpusLimitMessage(parsedResetTimestamp) + return { + statusCode: 403, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + error: 'opus_weekly_limit', + message: limitMessage + }), + accountId + } + } } else { isRateLimited = true if (!Number.isNaN(parsedResetTimestamp)) { @@ -883,6 +907,7 @@ class ClaudeRelayService { } const isDedicatedOfficialAccount = + accountType === 'claude-official' && apiKeyData.claudeAccountId && !apiKeyData.claudeAccountId.startsWith('group:') && apiKeyData.claudeAccountId === accountId @@ -893,6 +918,7 @@ class ClaudeRelayService { } if (isOpusModelRequest && isDedicatedOfficialAccount && opusRateLimitActive) { + const limitMessage = this._buildOpusLimitMessage(account?.opusRateLimitEndAt) if (!responseStream.headersSent) { responseStream.status(403) responseStream.setHeader('Content-Type', 'application/json') @@ -900,7 +926,7 @@ class ClaudeRelayService { responseStream.write( JSON.stringify({ error: 'opus_weekly_limit', - message: '此专属账号的Opus模型已达到本周使用限制,请尝试切换其他模型后再试。' + message: limitMessage }) ) responseStream.end() @@ -931,7 +957,8 @@ class ClaudeRelayService { accountType, sessionHash, streamTransformer, - options + options, + isDedicatedOfficialAccount ) } catch (error) { logger.error(`❌ Claude stream relay with usage capture failed:`, error) @@ -951,7 +978,8 @@ class ClaudeRelayService { accountType, sessionHash, streamTransformer = null, - requestOptions = {} + requestOptions = {}, + isDedicatedOfficialAccount = false ) { // 获取账户信息用于统一 User-Agent const account = await claudeAccountService.getAccount(accountId) @@ -1016,11 +1044,43 @@ class ClaudeRelayService { options.headers['anthropic-beta'] = betaHeader } - const req = https.request(options, (res) => { + const req = https.request(options, async (res) => { logger.debug(`🌊 Claude stream response status: ${res.statusCode}`) // 错误响应处理 if (res.statusCode !== 200) { + if (res.statusCode === 429 && isOpusModelRequest) { + const resetHeader = res.headers + ? res.headers['anthropic-ratelimit-unified-reset'] + : null + const parsedResetTimestamp = resetHeader ? parseInt(resetHeader, 10) : NaN + + if (!Number.isNaN(parsedResetTimestamp)) { + await claudeAccountService.markAccountOpusRateLimited(accountId, parsedResetTimestamp) + logger.warn( + `🚫 [Stream] Account ${accountId} hit Opus limit, resets at ${new Date(parsedResetTimestamp * 1000).toISOString()}` + ) + } + + if (isDedicatedOfficialAccount) { + const limitMessage = this._buildOpusLimitMessage(parsedResetTimestamp) + if (!responseStream.headersSent) { + responseStream.status(403) + responseStream.setHeader('Content-Type', 'application/json') + } + responseStream.write( + JSON.stringify({ + error: 'opus_weekly_limit', + message: limitMessage + }) + ) + responseStream.end() + res.resume() + resolve() + return + } + } + // 将错误处理逻辑封装在一个异步函数中 const handleErrorResponse = async () => { if (res.statusCode === 401) {