fix: claude subscription detection

This commit is contained in:
jett
2026-01-10 00:31:17 +08:00
parent 810fe9fe90
commit 28b27e6a7b

View File

@@ -28,44 +28,38 @@ class ClaudeRelayService {
} }
// 🔧 根据模型ID和客户端传递的 anthropic-beta 获取最终的 header // 🔧 根据模型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) { _getBetaHeader(modelId, clientBetaHeader) {
const OAUTH_BETA = 'oauth-2025-04-20' const OAUTH_BETA = 'oauth-2025-04-20'
const CLAUDE_CODE_BETA = 'claude-code-20250219' 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') const isHaikuModel = modelId && modelId.toLowerCase().includes('haiku')
if (isHaikuModel) { const baseBetas = isHaikuModel
return 'oauth-2025-04-20,interleaved-thinking-2025-05-14' ? [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) { _buildStandardRateLimitMessage(resetTime) {
@@ -140,6 +134,226 @@ class ClaudeRelayService {
return ClaudeCodeValidator.includesClaudeCodeSystemPrompt(requestBody, 1) 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`
}
_toRandomizedToolName(name) {
const suffix = Math.random().toString(36).substring(2, 8)
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 // 🚀 转发请求到Claude API
async relayRequest( async relayRequest(
requestBody, requestBody,
@@ -311,7 +525,9 @@ class ClaudeRelayService {
// 获取有效的访问token // 获取有效的访问token
const accessToken = await claudeAccountService.getValidAccessToken(accountId) const accessToken = await claudeAccountService.getValidAccessToken(accountId)
const isRealClaudeCodeRequest = this._isActualClaudeCodeRequest(requestBody, clientHeaders)
const processedBody = this._processRequestBody(requestBody, account) const processedBody = this._processRequestBody(requestBody, account)
const baseRequestBody = JSON.parse(JSON.stringify(processedBody))
// 获取代理配置 // 获取代理配置
const proxyAgent = await this._getProxyAgent(accountId) const proxyAgent = await this._getProxyAgent(accountId)
@@ -332,36 +548,53 @@ class ClaudeRelayService {
clientResponse.once('close', handleClientDisconnect) clientResponse.once('close', handleClientDisconnect)
} }
// 发送请求到Claude API传入回调以获取请求对象 const makeRequestWithRetries = async (requestOptions) => {
// 🔄 403 重试机制:仅对 claude-official 类型账户OAuth 或 Setup Token const maxRetries = this._shouldRetryOn403(accountType) ? 2 : 0
const maxRetries = this._shouldRetryOn403(accountType) ? 2 : 0 let retryCount = 0
let retryCount = 0 let response
let response let shouldRetry = false
let shouldRetry = false
do { do {
response = await this._makeClaudeRequest( response = await this._makeClaudeRequest(
processedBody, JSON.parse(JSON.stringify(baseRequestBody)),
accessToken, accessToken,
proxyAgent, proxyAgent,
clientHeaders, clientHeaders,
accountId, accountId,
(req) => { (req) => {
upstreamRequest = req upstreamRequest = req
}, },
options {
) ...requestOptions,
isRealClaudeCodeRequest
// 检查是否需要重试 403 }
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)
} shouldRetry = response.statusCode === 403 && retryCount < maxRetries
} while (shouldRetry) 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 }
const retryResult = await makeRequestWithRetries(requestOptions)
response = retryResult.response
retryCount = retryResult.retryCount
}
// 如果进行了重试,记录最终结果 // 如果进行了重试,记录最终结果
if (retryCount > 0) { if (retryCount > 0) {
@@ -1035,23 +1268,19 @@ class ClaudeRelayService {
// 获取过滤后的客户端 headers // 获取过滤后的客户端 headers
const filteredHeaders = this._filterClientHeaders(clientHeaders) const filteredHeaders = this._filterClientHeaders(clientHeaders)
// 判断是否是真实的 Claude Code 请求 const isRealClaudeCode =
const isRealClaudeCode = this.isRealClaudeCodeRequest(body) requestOptions.isRealClaudeCodeRequest === undefined
? this.isRealClaudeCodeRequest(body)
: requestOptions.isRealClaudeCodeRequest === true
// 如果不是真实的 Claude Code 请求,需要使用从账户获取的 Claude Code headers // 如果不是真实的 Claude Code 请求,需要使用从账户获取的 Claude Code headers
let finalHeaders = { ...filteredHeaders } let finalHeaders = { ...filteredHeaders }
let requestPayload = body let requestPayload = body
if (!isRealClaudeCode) { if (!isRealClaudeCode) {
// 获取该账号存储的 Claude Code headers
const claudeCodeHeaders = await claudeCodeHeadersService.getAccountHeaders(accountId) const claudeCodeHeaders = await claudeCodeHeadersService.getAccountHeaders(accountId)
// 只添加客户端没有提供的 headers
Object.keys(claudeCodeHeaders).forEach((key) => { Object.keys(claudeCodeHeaders).forEach((key) => {
const lowerKey = key.toLowerCase() finalHeaders[key] = claudeCodeHeaders[key]
if (!finalHeaders[key] && !finalHeaders[lowerKey]) {
finalHeaders[key] = claudeCodeHeaders[key]
}
}) })
} }
@@ -1073,6 +1302,13 @@ class ClaudeRelayService {
requestPayload = extensionResult.body requestPayload = extensionResult.body
finalHeaders = extensionResult.headers finalHeaders = extensionResult.headers
let toolNameMap = null
if (!isRealClaudeCode) {
toolNameMap = this._transformToolNamesInRequestBody(requestPayload, {
useRandomizedToolNames: requestOptions.useRandomizedToolNames === true
})
}
// 序列化请求体,计算 content-length // 序列化请求体,计算 content-length
const bodyString = JSON.stringify(requestPayload) const bodyString = JSON.stringify(requestPayload)
const contentLength = Buffer.byteLength(bodyString, 'utf8') const contentLength = Buffer.byteLength(bodyString, 'utf8')
@@ -1098,17 +1334,16 @@ class ClaudeRelayService {
logger.info(`🔗 指纹是这个: ${headers['User-Agent']}`) logger.info(`🔗 指纹是这个: ${headers['User-Agent']}`)
logger.info(`🔗 指纹是这个: ${headers['User-Agent']}`)
// 根据模型和客户端传递的 anthropic-beta 动态设置 header // 根据模型和客户端传递的 anthropic-beta 动态设置 header
const modelId = requestPayload?.model || body?.model 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) headers['anthropic-beta'] = this._getBetaHeader(modelId, clientBetaHeader)
return { return {
requestPayload, requestPayload,
bodyString, bodyString,
headers, headers,
isRealClaudeCode isRealClaudeCode,
toolNameMap
} }
} }
@@ -1174,7 +1409,7 @@ class ClaudeRelayService {
return prepared.abortResponse return prepared.abortResponse
} }
const { bodyString, headers } = prepared const { bodyString, headers, isRealClaudeCode, toolNameMap } = prepared
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 支持自定义路径(如 count_tokens // 支持自定义路径(如 count_tokens
@@ -1226,6 +1461,10 @@ class ClaudeRelayService {
responseBody = responseData.toString('utf8') responseBody = responseData.toString('utf8')
} }
if (!isRealClaudeCode) {
responseBody = this._restoreToolNamesInResponseBody(responseBody, toolNameMap)
}
const response = { const response = {
statusCode: res.statusCode, statusCode: res.statusCode,
headers: res.headers, headers: res.headers,
@@ -1465,14 +1704,16 @@ class ClaudeRelayService {
// 获取有效的访问token // 获取有效的访问token
const accessToken = await claudeAccountService.getValidAccessToken(accountId) const accessToken = await claudeAccountService.getValidAccessToken(accountId)
const isRealClaudeCodeRequest = this._isActualClaudeCodeRequest(requestBody, clientHeaders)
const processedBody = this._processRequestBody(requestBody, account) const processedBody = this._processRequestBody(requestBody, account)
const baseRequestBody = JSON.parse(JSON.stringify(processedBody))
// 获取代理配置 // 获取代理配置
const proxyAgent = await this._getProxyAgent(accountId) const proxyAgent = await this._getProxyAgent(accountId)
// 发送流式请求并捕获usage数据 // 发送流式请求并捕获usage数据
await this._makeClaudeStreamRequestWithUsageCapture( await this._makeClaudeStreamRequestWithUsageCapture(
processedBody, JSON.parse(JSON.stringify(baseRequestBody)),
accessToken, accessToken,
proxyAgent, proxyAgent,
clientHeaders, clientHeaders,
@@ -1487,7 +1728,11 @@ class ClaudeRelayService {
accountType, accountType,
sessionHash, sessionHash,
streamTransformer, streamTransformer,
options, {
...options,
originalRequestBody: baseRequestBody,
isRealClaudeCodeRequest
},
isDedicatedOfficialAccount, isDedicatedOfficialAccount,
// 📬 新增回调:在收到响应头时释放队列锁 // 📬 新增回调:在收到响应头时释放队列锁
async () => { async () => {
@@ -1576,7 +1821,11 @@ class ClaudeRelayService {
return prepared.abortResponse return prepared.abortResponse
} }
const { bodyString, headers } = prepared const { bodyString, headers, isRealClaudeCode, toolNameMap } = prepared
const toolNameStreamTransformer = this._createToolNameStripperStreamTransformer(
streamTransformer,
toolNameMap
)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = new URL(this.claudeApiUrl) const url = new URL(this.claudeApiUrl)
@@ -1684,8 +1933,11 @@ class ClaudeRelayService {
try { try {
// 递归调用自身进行重试 // 递归调用自身进行重试
const retryBody = requestOptions.originalRequestBody
? JSON.parse(JSON.stringify(requestOptions.originalRequestBody))
: body
const retryResult = await this._makeClaudeStreamRequestWithUsageCapture( const retryResult = await this._makeClaudeStreamRequestWithUsageCapture(
body, retryBody,
accessToken, accessToken,
proxyAgent, proxyAgent,
clientHeaders, clientHeaders,
@@ -1780,11 +2032,40 @@ class ClaudeRelayService {
errorData += chunk.toString() errorData += chunk.toString()
}) })
res.on('end', () => { res.on('end', async () => {
logger.error( logger.error(
`❌ Claude API error response (Account: ${account?.name || accountId}):`, `❌ Claude API error response (Account: ${account?.name || accountId}):`,
errorData errorData
) )
if (
this._isClaudeCodeCredentialError(errorData) &&
requestOptions.useRandomizedToolNames !== true &&
requestOptions.originalRequestBody
) {
try {
const retryBody = JSON.parse(JSON.stringify(requestOptions.originalRequestBody))
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)) { if (this._isOrganizationDisabledError(res.statusCode, errorData)) {
;(async () => { ;(async () => {
try { try {
@@ -1819,7 +2100,7 @@ class ClaudeRelayService {
} }
// 如果有 streamTransformer如测试请求使用前端期望的格式 // 如果有 streamTransformer如测试请求使用前端期望的格式
if (streamTransformer) { if (toolNameStreamTransformer) {
responseStream.write( responseStream.write(
`data: ${JSON.stringify({ type: 'error', error: errorMessage })}\n\n` `data: ${JSON.stringify({ type: 'error', error: errorMessage })}\n\n`
) )
@@ -1873,8 +2154,8 @@ class ClaudeRelayService {
if (isStreamWritable(responseStream)) { if (isStreamWritable(responseStream)) {
const linesToForward = lines.join('\n') + (lines.length > 0 ? '\n' : '') const linesToForward = lines.join('\n') + (lines.length > 0 ? '\n' : '')
// 如果有流转换器,应用转换 // 如果有流转换器,应用转换
if (streamTransformer) { if (toolNameStreamTransformer) {
const transformed = streamTransformer(linesToForward) const transformed = toolNameStreamTransformer(linesToForward)
if (transformed) { if (transformed) {
responseStream.write(transformed) responseStream.write(transformed)
} }
@@ -2007,8 +2288,8 @@ class ClaudeRelayService {
try { try {
// 处理缓冲区中剩余的数据 // 处理缓冲区中剩余的数据
if (buffer.trim() && isStreamWritable(responseStream)) { if (buffer.trim() && isStreamWritable(responseStream)) {
if (streamTransformer) { if (toolNameStreamTransformer) {
const transformed = streamTransformer(buffer) const transformed = toolNameStreamTransformer(buffer)
if (transformed) { if (transformed) {
responseStream.write(transformed) responseStream.write(transformed)
} }