This commit is contained in:
SunSeekerX
2026-01-21 11:55:28 +08:00
149 changed files with 15035 additions and 4017 deletions

View File

@@ -29,51 +29,51 @@ const safeClone =
class ClaudeRelayService {
constructor() {
this.claudeApiUrl = 'https://api.anthropic.com/v1/messages?beta=true'
// 🧹 内存优化:用于存储请求体字符串,避免闭包捕获
this.bodyStore = new Map()
this._bodyStoreIdCounter = 0
this.apiVersion = config.claude.apiVersion
this.betaHeader = config.claude.betaHeader
this.systemPrompt = config.claude.systemPrompt
this.claudeCodeSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude."
this.toolNameSuffix = null
this.toolNameSuffixGeneratedAt = 0
this.toolNameSuffixTtlMs = 60 * 60 * 1000
}
// 🔧 根据模型ID和客户端传递的 anthropic-beta 获取最终的 header
// 规则:
// 1. 如果客户端传递了 anthropic-beta检查是否包含 oauth-2025-04-20
// 2. 如果没有 oauth-2025-04-20则添加到 claude-code-20250219 后面(如果有的话),否则放在第一位
// 3. 如果客户端没传递则根据模型判断haiku 不需要 claude-code其他模型需要
_getBetaHeader(modelId, clientBetaHeader) {
const OAUTH_BETA = 'oauth-2025-04-20'
const CLAUDE_CODE_BETA = 'claude-code-20250219'
const INTERLEAVED_THINKING_BETA = 'interleaved-thinking-2025-05-14'
const TOOL_STREAMING_BETA = 'fine-grained-tool-streaming-2025-05-14'
// 如果客户端传递了 anthropic-beta
if (clientBetaHeader) {
// 检查是否已包含 oauth-2025-04-20
if (clientBetaHeader.includes(OAUTH_BETA)) {
return clientBetaHeader
}
// 需要添加 oauth-2025-04-20
const parts = clientBetaHeader.split(',').map((p) => p.trim())
// 找到 claude-code-20250219 的位置
const claudeCodeIndex = parts.findIndex((p) => p === CLAUDE_CODE_BETA)
if (claudeCodeIndex !== -1) {
// 在 claude-code-20250219 后面插入
parts.splice(claudeCodeIndex + 1, 0, OAUTH_BETA)
} else {
// 放在第一位
parts.unshift(OAUTH_BETA)
}
return parts.join(',')
}
// 客户端没有传递,根据模型判断
const isHaikuModel = modelId && modelId.toLowerCase().includes('haiku')
if (isHaikuModel) {
return 'oauth-2025-04-20,interleaved-thinking-2025-05-14'
const baseBetas = isHaikuModel
? [OAUTH_BETA, INTERLEAVED_THINKING_BETA]
: [CLAUDE_CODE_BETA, OAUTH_BETA, INTERLEAVED_THINKING_BETA, TOOL_STREAMING_BETA]
const betaList = []
const seen = new Set()
const addBeta = (beta) => {
if (!beta || seen.has(beta)) {
return
}
seen.add(beta)
betaList.push(beta)
}
return 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14'
baseBetas.forEach(addBeta)
if (clientBetaHeader) {
clientBetaHeader
.split(',')
.map((p) => p.trim())
.filter(Boolean)
.forEach(addBeta)
}
return betaList.join(',')
}
_buildStandardRateLimitMessage(resetTime) {
@@ -148,6 +148,235 @@ class ClaudeRelayService {
return ClaudeCodeValidator.includesClaudeCodeSystemPrompt(requestBody, 1)
}
_isClaudeCodeUserAgent(clientHeaders) {
const userAgent = clientHeaders?.['user-agent'] || clientHeaders?.['User-Agent']
return typeof userAgent === 'string' && /^claude-cli\/[^\s]+\s+\(/i.test(userAgent)
}
_isActualClaudeCodeRequest(requestBody, clientHeaders) {
return this.isRealClaudeCodeRequest(requestBody) && this._isClaudeCodeUserAgent(clientHeaders)
}
_getHeaderValueCaseInsensitive(headers, key) {
if (!headers || typeof headers !== 'object') {
return undefined
}
const lowerKey = key.toLowerCase()
for (const candidate of Object.keys(headers)) {
if (candidate.toLowerCase() === lowerKey) {
return headers[candidate]
}
}
return undefined
}
_isClaudeCodeCredentialError(body) {
const message = this._extractErrorMessage(body)
if (!message) {
return false
}
const lower = message.toLowerCase()
return (
lower.includes('only authorized for use with claude code') ||
lower.includes('cannot be used for other api requests')
)
}
_toPascalCaseToolName(name) {
const parts = name.split(/[_-]/).filter(Boolean)
if (parts.length === 0) {
return name
}
const pascal = parts
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
.join('')
return `${pascal}_tool`
}
_getToolNameSuffix() {
const now = Date.now()
if (!this.toolNameSuffix || now - this.toolNameSuffixGeneratedAt > this.toolNameSuffixTtlMs) {
this.toolNameSuffix = Math.random().toString(36).substring(2, 8)
this.toolNameSuffixGeneratedAt = now
}
return this.toolNameSuffix
}
_toRandomizedToolName(name) {
const suffix = this._getToolNameSuffix()
return `${name}_${suffix}`
}
_transformToolNamesInRequestBody(body, options = {}) {
if (!body || typeof body !== 'object') {
return null
}
const useRandomized = options.useRandomizedToolNames === true
const forwardMap = new Map()
const reverseMap = new Map()
const transformName = (name) => {
if (typeof name !== 'string' || name.length === 0) {
return name
}
if (forwardMap.has(name)) {
return forwardMap.get(name)
}
const transformed = useRandomized
? this._toRandomizedToolName(name)
: this._toPascalCaseToolName(name)
if (transformed !== name) {
forwardMap.set(name, transformed)
reverseMap.set(transformed, name)
}
return transformed
}
if (Array.isArray(body.tools)) {
body.tools.forEach((tool) => {
if (tool && typeof tool.name === 'string') {
tool.name = transformName(tool.name)
}
})
}
if (body.tool_choice && typeof body.tool_choice === 'object') {
if (typeof body.tool_choice.name === 'string') {
body.tool_choice.name = transformName(body.tool_choice.name)
}
}
if (Array.isArray(body.messages)) {
body.messages.forEach((message) => {
const content = message?.content
if (Array.isArray(content)) {
content.forEach((block) => {
if (block?.type === 'tool_use' && typeof block.name === 'string') {
block.name = transformName(block.name)
}
})
}
})
}
return reverseMap.size > 0 ? reverseMap : null
}
_restoreToolName(name, toolNameMap) {
if (!toolNameMap || toolNameMap.size === 0) {
return name
}
return toolNameMap.get(name) || name
}
_restoreToolNamesInContentBlocks(content, toolNameMap) {
if (!Array.isArray(content)) {
return
}
content.forEach((block) => {
if (block?.type === 'tool_use' && typeof block.name === 'string') {
block.name = this._restoreToolName(block.name, toolNameMap)
}
})
}
_restoreToolNamesInResponseObject(responseBody, toolNameMap) {
if (!responseBody || typeof responseBody !== 'object') {
return
}
if (Array.isArray(responseBody.content)) {
this._restoreToolNamesInContentBlocks(responseBody.content, toolNameMap)
}
if (responseBody.message && Array.isArray(responseBody.message.content)) {
this._restoreToolNamesInContentBlocks(responseBody.message.content, toolNameMap)
}
}
_restoreToolNamesInResponseBody(responseBody, toolNameMap) {
if (!responseBody || !toolNameMap || toolNameMap.size === 0) {
return responseBody
}
if (typeof responseBody === 'string') {
try {
const parsed = JSON.parse(responseBody)
this._restoreToolNamesInResponseObject(parsed, toolNameMap)
return JSON.stringify(parsed)
} catch (error) {
return responseBody
}
}
if (typeof responseBody === 'object') {
this._restoreToolNamesInResponseObject(responseBody, toolNameMap)
}
return responseBody
}
_restoreToolNamesInStreamEvent(event, toolNameMap) {
if (!event || typeof event !== 'object') {
return
}
if (event.content_block && event.content_block.type === 'tool_use') {
if (typeof event.content_block.name === 'string') {
event.content_block.name = this._restoreToolName(event.content_block.name, toolNameMap)
}
}
if (event.delta && event.delta.type === 'tool_use') {
if (typeof event.delta.name === 'string') {
event.delta.name = this._restoreToolName(event.delta.name, toolNameMap)
}
}
if (event.message && Array.isArray(event.message.content)) {
this._restoreToolNamesInContentBlocks(event.message.content, toolNameMap)
}
if (Array.isArray(event.content)) {
this._restoreToolNamesInContentBlocks(event.content, toolNameMap)
}
}
_createToolNameStripperStreamTransformer(streamTransformer, toolNameMap) {
if (!toolNameMap || toolNameMap.size === 0) {
return streamTransformer
}
return (payload) => {
const transformed = streamTransformer ? streamTransformer(payload) : payload
if (!transformed || typeof transformed !== 'string') {
return transformed
}
const lines = transformed.split('\n')
const updated = lines.map((line) => {
if (!line.startsWith('data:')) {
return line
}
const jsonStr = line.slice(5).trimStart()
if (!jsonStr || jsonStr === '[DONE]') {
return line
}
try {
const data = JSON.parse(jsonStr)
this._restoreToolNamesInStreamEvent(data, toolNameMap)
return `data: ${JSON.stringify(data)}`
} catch (error) {
return line
}
})
return updated.join('\n')
}
}
// 🚀 转发请求到Claude API
async relayRequest(
requestBody,
@@ -161,6 +390,7 @@ class ClaudeRelayService {
let queueLockAcquired = false
let queueRequestId = null
let selectedAccountId = null
let bodyStoreIdNonStream = null // 🧹 在 try 块外声明,以便 finally 清理
try {
// 调试日志查看API Key数据
@@ -319,7 +549,12 @@ class ClaudeRelayService {
// 获取有效的访问token
const accessToken = await claudeAccountService.getValidAccessToken(accountId)
const isRealClaudeCodeRequest = this._isActualClaudeCodeRequest(requestBody, clientHeaders)
const processedBody = this._processRequestBody(requestBody, account)
// 🧹 内存优化:存储到 bodyStore避免闭包捕获
const originalBodyString = JSON.stringify(processedBody)
bodyStoreIdNonStream = ++this._bodyStoreIdCounter
this.bodyStore.set(bodyStoreIdNonStream, originalBodyString)
// 获取代理配置
const proxyAgent = await this._getProxyAgent(accountId)
@@ -340,36 +575,59 @@ class ClaudeRelayService {
clientResponse.once('close', handleClientDisconnect)
}
// 发送请求到Claude API传入回调以获取请求对象
// 🔄 403 重试机制:仅对 claude-official 类型账户OAuth 或 Setup Token
const maxRetries = this._shouldRetryOn403(accountType) ? 2 : 0
let retryCount = 0
let response
let shouldRetry = false
const makeRequestWithRetries = async (requestOptions) => {
const maxRetries = this._shouldRetryOn403(accountType) ? 2 : 0
let retryCount = 0
let response
let shouldRetry = false
do {
response = await this._makeClaudeRequest(
processedBody,
accessToken,
proxyAgent,
clientHeaders,
accountId,
(req) => {
upstreamRequest = req
},
options
)
// 检查是否需要重试 403
shouldRetry = response.statusCode === 403 && retryCount < maxRetries
if (shouldRetry) {
retryCount++
logger.warn(
`🔄 403 error for account ${accountId}, retry ${retryCount}/${maxRetries} after 2s`
do {
// 🧹 每次重试从 bodyStore 解析新对象,避免闭包捕获
let retryRequestBody
try {
retryRequestBody = JSON.parse(this.bodyStore.get(bodyStoreIdNonStream))
} catch (parseError) {
logger.error(`❌ Failed to parse body for retry: ${parseError.message}`)
throw new Error(`Request body parse failed: ${parseError.message}`)
}
response = await this._makeClaudeRequest(
retryRequestBody,
accessToken,
proxyAgent,
clientHeaders,
accountId,
(req) => {
upstreamRequest = req
},
{
...requestOptions,
isRealClaudeCodeRequest
}
)
await this._sleep(2000)
}
} while (shouldRetry)
shouldRetry = response.statusCode === 403 && retryCount < maxRetries
if (shouldRetry) {
retryCount++
logger.warn(
`🔄 403 error for account ${accountId}, retry ${retryCount}/${maxRetries} after 2s`
)
await this._sleep(2000)
}
} while (shouldRetry)
return { response, retryCount }
}
let requestOptions = options
let { response, retryCount } = await makeRequestWithRetries(requestOptions)
if (
this._isClaudeCodeCredentialError(response.body) &&
requestOptions.useRandomizedToolNames !== true
) {
requestOptions = { ...requestOptions, useRandomizedToolNames: true }
;({ response, retryCount } = await makeRequestWithRetries(requestOptions))
}
// 如果进行了重试,记录最终结果
if (retryCount > 0) {
@@ -669,6 +927,10 @@ class ClaudeRelayService {
)
throw error
} finally {
// 🧹 清理 bodyStore
if (bodyStoreIdNonStream !== null) {
this.bodyStore.delete(bodyStoreIdNonStream)
}
// 📬 释放用户消息队列锁(兜底,正常情况下已在请求发送后提前释放)
if (queueLockAcquired && queueRequestId && selectedAccountId) {
try {
@@ -1043,23 +1305,19 @@ class ClaudeRelayService {
// 获取过滤后的客户端 headers
const filteredHeaders = this._filterClientHeaders(clientHeaders)
// 判断是否是真实的 Claude Code 请求
const isRealClaudeCode = this.isRealClaudeCodeRequest(body)
const isRealClaudeCode =
requestOptions.isRealClaudeCodeRequest === undefined
? this.isRealClaudeCodeRequest(body)
: requestOptions.isRealClaudeCodeRequest === true
// 如果不是真实的 Claude Code 请求,需要使用从账户获取的 Claude Code headers
let finalHeaders = { ...filteredHeaders }
let requestPayload = body
if (!isRealClaudeCode) {
// 获取该账号存储的 Claude Code headers
const claudeCodeHeaders = await claudeCodeHeadersService.getAccountHeaders(accountId)
// 只添加客户端没有提供的 headers
Object.keys(claudeCodeHeaders).forEach((key) => {
const lowerKey = key.toLowerCase()
if (!finalHeaders[key] && !finalHeaders[lowerKey]) {
finalHeaders[key] = claudeCodeHeaders[key]
}
finalHeaders[key] = claudeCodeHeaders[key]
})
}
@@ -1081,6 +1339,13 @@ class ClaudeRelayService {
requestPayload = extensionResult.body
finalHeaders = extensionResult.headers
let toolNameMap = null
if (!isRealClaudeCode) {
toolNameMap = this._transformToolNamesInRequestBody(requestPayload, {
useRandomizedToolNames: requestOptions.useRandomizedToolNames === true
})
}
// 序列化请求体,计算 content-length
const bodyString = JSON.stringify(requestPayload)
const contentLength = Buffer.byteLength(bodyString, 'utf8')
@@ -1108,13 +1373,14 @@ class ClaudeRelayService {
// 根据模型和客户端传递的 anthropic-beta 动态设置 header
const modelId = requestPayload?.model || body?.model
const clientBetaHeader = clientHeaders?.['anthropic-beta']
const clientBetaHeader = this._getHeaderValueCaseInsensitive(clientHeaders, 'anthropic-beta')
headers['anthropic-beta'] = this._getBetaHeader(modelId, clientBetaHeader)
return {
requestPayload,
bodyString,
headers,
isRealClaudeCode
isRealClaudeCode,
toolNameMap
}
}
@@ -1180,7 +1446,8 @@ class ClaudeRelayService {
return prepared.abortResponse
}
const { bodyString, headers } = prepared
let { bodyString } = prepared
const { headers, isRealClaudeCode, toolNameMap } = prepared
return new Promise((resolve, reject) => {
// 支持自定义路径(如 count_tokens
@@ -1235,6 +1502,10 @@ class ClaudeRelayService {
responseBody = responseData.toString('utf8')
}
if (!isRealClaudeCode) {
responseBody = this._restoreToolNamesInResponseBody(responseBody, toolNameMap)
}
const response = {
statusCode: res.statusCode,
headers: res.headers,
@@ -1293,6 +1564,8 @@ class ClaudeRelayService {
// 写入请求体
req.write(bodyString)
// 🧹 内存优化:立即清空 bodyString 引用,避免闭包捕获
bodyString = null
req.end()
})
}
@@ -1474,7 +1747,12 @@ class ClaudeRelayService {
// 获取有效的访问token
const accessToken = await claudeAccountService.getValidAccessToken(accountId)
const isRealClaudeCodeRequest = this._isActualClaudeCodeRequest(requestBody, clientHeaders)
const processedBody = this._processRequestBody(requestBody, account)
// 🧹 内存优化:存储到 bodyStore不放入 requestOptions 避免闭包捕获
const originalBodyString = JSON.stringify(processedBody)
const bodyStoreId = ++this._bodyStoreIdCounter
this.bodyStore.set(bodyStoreId, originalBodyString)
// 获取代理配置
const proxyAgent = await this._getProxyAgent(accountId)
@@ -1496,7 +1774,11 @@ class ClaudeRelayService {
accountType,
sessionHash,
streamTransformer,
options,
{
...options,
bodyStoreId,
isRealClaudeCodeRequest
},
isDedicatedOfficialAccount,
// 📬 新增回调:在收到响应头时释放队列锁
async () => {
@@ -1585,7 +1867,12 @@ class ClaudeRelayService {
return prepared.abortResponse
}
const { bodyString, headers } = prepared
let { bodyString } = prepared
const { headers, toolNameMap } = prepared
const toolNameStreamTransformer = this._createToolNameStripperStreamTransformer(
streamTransformer,
toolNameMap
)
return new Promise((resolve, reject) => {
const url = new URL(this.claudeApiUrl)
@@ -1693,8 +1980,22 @@ class ClaudeRelayService {
try {
// 递归调用自身进行重试
// 🧹 从 bodyStore 获取字符串用于重试
if (
!requestOptions.bodyStoreId ||
!this.bodyStore.has(requestOptions.bodyStoreId)
) {
throw new Error('529 retry requires valid bodyStoreId')
}
let retryBody
try {
retryBody = JSON.parse(this.bodyStore.get(requestOptions.bodyStoreId))
} catch (parseError) {
logger.error(`❌ Failed to parse body for 529 retry: ${parseError.message}`)
throw new Error(`529 retry body parse failed: ${parseError.message}`)
}
const retryResult = await this._makeClaudeStreamRequestWithUsageCapture(
body,
retryBody,
accessToken,
proxyAgent,
clientHeaders,
@@ -1789,11 +2090,48 @@ class ClaudeRelayService {
errorData += chunk.toString()
})
res.on('end', () => {
res.on('end', async () => {
logger.error(
`❌ Claude API error response (Account: ${account?.name || accountId}):`,
errorData
)
if (
this._isClaudeCodeCredentialError(errorData) &&
requestOptions.useRandomizedToolNames !== true &&
requestOptions.bodyStoreId &&
this.bodyStore.has(requestOptions.bodyStoreId)
) {
let retryBody
try {
retryBody = JSON.parse(this.bodyStore.get(requestOptions.bodyStoreId))
} catch (parseError) {
logger.error(`❌ Failed to parse body for 403 retry: ${parseError.message}`)
reject(new Error(`403 retry body parse failed: ${parseError.message}`))
return
}
try {
const retryResult = await this._makeClaudeStreamRequestWithUsageCapture(
retryBody,
accessToken,
proxyAgent,
clientHeaders,
responseStream,
usageCallback,
accountId,
accountType,
sessionHash,
streamTransformer,
{ ...requestOptions, useRandomizedToolNames: true },
isDedicatedOfficialAccount,
onResponseStart,
retryCount
)
resolve(retryResult)
} catch (retryError) {
reject(retryError)
}
return
}
if (this._isOrganizationDisabledError(res.statusCode, errorData)) {
;(async () => {
try {
@@ -1828,7 +2166,7 @@ class ClaudeRelayService {
}
// 如果有 streamTransformer如测试请求使用前端期望的格式
if (streamTransformer) {
if (toolNameStreamTransformer) {
responseStream.write(
`data: ${JSON.stringify({ type: 'error', error: errorMessage })}\n\n`
)
@@ -1867,6 +2205,11 @@ class ClaudeRelayService {
let rateLimitDetected = false // 限流检测标志
// 监听数据块解析SSE并寻找usage信息
// 🧹 内存优化:在闭包创建前提取需要的值,避免闭包捕获 body 和 requestOptions
// body 和 requestOptions 只在闭包外使用,闭包内只引用基本类型
const requestedModel = body?.model || 'unknown'
const { isRealClaudeCodeRequest } = requestOptions
res.on('data', (chunk) => {
try {
const chunkStr = chunk.toString()
@@ -1882,8 +2225,8 @@ class ClaudeRelayService {
if (isStreamWritable(responseStream)) {
const linesToForward = lines.join('\n') + (lines.length > 0 ? '\n' : '')
// 如果有流转换器,应用转换
if (streamTransformer) {
const transformed = streamTransformer(linesToForward)
if (toolNameStreamTransformer) {
const transformed = toolNameStreamTransformer(linesToForward)
if (transformed) {
responseStream.write(transformed)
}
@@ -2016,8 +2359,8 @@ class ClaudeRelayService {
try {
// 处理缓冲区中剩余的数据
if (buffer.trim() && isStreamWritable(responseStream)) {
if (streamTransformer) {
const transformed = streamTransformer(buffer)
if (toolNameStreamTransformer) {
const transformed = toolNameStreamTransformer(buffer)
if (transformed) {
responseStream.write(transformed)
}
@@ -2072,7 +2415,7 @@ class ClaudeRelayService {
// 打印原始的usage数据为JSON字符串避免嵌套问题
logger.info(
`📊 === Stream Request Usage Summary === Model: ${body.model}, Total Events: ${allUsageData.length}, Usage Data: ${JSON.stringify(allUsageData)}`
`📊 === Stream Request Usage Summary === Model: ${requestedModel}, Total Events: ${allUsageData.length}, Usage Data: ${JSON.stringify(allUsageData)}`
)
// 一般一个请求只会使用一个模型即使有多个usage事件也应该合并
@@ -2082,7 +2425,7 @@ class ClaudeRelayService {
output_tokens: totalUsage.output_tokens,
cache_creation_input_tokens: totalUsage.cache_creation_input_tokens,
cache_read_input_tokens: totalUsage.cache_read_input_tokens,
model: allUsageData[allUsageData.length - 1].model || body.model // 使用最后一个模型或请求模型
model: allUsageData[allUsageData.length - 1].model || requestedModel // 使用最后一个模型或请求模型
}
// 如果有详细的cache_creation数据合并它们
@@ -2191,15 +2534,15 @@ class ClaudeRelayService {
}
// 只有真实的 Claude Code 请求才更新 headers流式请求
if (
clientHeaders &&
Object.keys(clientHeaders).length > 0 &&
this.isRealClaudeCodeRequest(body)
) {
if (clientHeaders && Object.keys(clientHeaders).length > 0 && isRealClaudeCodeRequest) {
await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders)
}
}
// 🧹 清理 bodyStore
if (requestOptions.bodyStoreId) {
this.bodyStore.delete(requestOptions.bodyStoreId)
}
logger.debug('🌊 Claude stream response with usage capture completed')
resolve()
})
@@ -2256,6 +2599,10 @@ class ClaudeRelayService {
)
responseStream.end()
}
// 🧹 清理 bodyStore
if (requestOptions.bodyStoreId) {
this.bodyStore.delete(requestOptions.bodyStoreId)
}
reject(error)
})
@@ -2285,6 +2632,10 @@ class ClaudeRelayService {
)
responseStream.end()
}
// 🧹 清理 bodyStore
if (requestOptions.bodyStoreId) {
this.bodyStore.delete(requestOptions.bodyStoreId)
}
reject(new Error('Request timeout'))
})
@@ -2298,6 +2649,8 @@ class ClaudeRelayService {
// 写入请求体
req.write(bodyString)
// 🧹 内存优化:立即清空 bodyString 引用,避免闭包捕获
bodyString = null
req.end()
})
}