mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: claude subscription detection
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
return 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14'
|
seen.add(beta)
|
||||||
|
betaList.push(beta)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,8 +548,7 @@ 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
|
||||||
@@ -341,7 +556,7 @@ class ClaudeRelayService {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
response = await this._makeClaudeRequest(
|
response = await this._makeClaudeRequest(
|
||||||
processedBody,
|
JSON.parse(JSON.stringify(baseRequestBody)),
|
||||||
accessToken,
|
accessToken,
|
||||||
proxyAgent,
|
proxyAgent,
|
||||||
clientHeaders,
|
clientHeaders,
|
||||||
@@ -349,10 +564,12 @@ class ClaudeRelayService {
|
|||||||
(req) => {
|
(req) => {
|
||||||
upstreamRequest = req
|
upstreamRequest = req
|
||||||
},
|
},
|
||||||
options
|
{
|
||||||
|
...requestOptions,
|
||||||
|
isRealClaudeCodeRequest
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 检查是否需要重试 403
|
|
||||||
shouldRetry = response.statusCode === 403 && retryCount < maxRetries
|
shouldRetry = response.statusCode === 403 && retryCount < maxRetries
|
||||||
if (shouldRetry) {
|
if (shouldRetry) {
|
||||||
retryCount++
|
retryCount++
|
||||||
@@ -363,6 +580,22 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
} while (shouldRetry)
|
} 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) {
|
||||||
if (response.statusCode === 403) {
|
if (response.statusCode === 403) {
|
||||||
@@ -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()
|
|
||||||
if (!finalHeaders[key] && !finalHeaders[lowerKey]) {
|
|
||||||
finalHeaders[key] = claudeCodeHeaders[key]
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user