mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-27 18:37:39 +00:00
1
This commit is contained in:
@@ -20,16 +20,21 @@ const {
|
||||
sendMockWarmupStream
|
||||
} = require('../utils/warmupInterceptor')
|
||||
const { sanitizeUpstreamError } = require('../utils/errorSanitizer')
|
||||
const { dumpAnthropicMessagesRequest } = require('../utils/anthropicRequestDump')
|
||||
const {
|
||||
handleAnthropicMessagesToGemini,
|
||||
handleAnthropicCountTokensToGemini
|
||||
} = require('../services/anthropicGeminiBridgeService')
|
||||
const router = express.Router()
|
||||
|
||||
function queueRateLimitUpdate(rateLimitInfo, usageSummary, model, context = '') {
|
||||
function queueRateLimitUpdate(rateLimitInfo, usageSummary, model, context = '', keyId = null, accountType = null) {
|
||||
if (!rateLimitInfo) {
|
||||
return Promise.resolve({ totalTokens: 0, totalCost: 0 })
|
||||
}
|
||||
|
||||
const label = context ? ` (${context})` : ''
|
||||
|
||||
return updateRateLimitCounters(rateLimitInfo, usageSummary, model)
|
||||
return updateRateLimitCounters(rateLimitInfo, usageSummary, model, keyId, accountType)
|
||||
.then(({ totalTokens, totalCost }) => {
|
||||
if (totalTokens > 0) {
|
||||
logger.api(`📊 Updated rate limit token count${label}: +${totalTokens} tokens`)
|
||||
@@ -117,16 +122,18 @@ async function handleMessagesRequest(req, res) {
|
||||
try {
|
||||
const startTime = Date.now()
|
||||
|
||||
// Claude 服务权限校验,阻止未授权的 Key
|
||||
if (
|
||||
req.apiKey.permissions &&
|
||||
req.apiKey.permissions !== 'all' &&
|
||||
req.apiKey.permissions !== 'claude'
|
||||
) {
|
||||
const forcedVendor = req._anthropicVendor || null
|
||||
const requiredService =
|
||||
forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity' ? 'gemini' : 'claude'
|
||||
|
||||
if (!apiKeyService.hasPermission(req.apiKey?.permissions, requiredService)) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
message: '此 API Key 无权访问 Claude 服务'
|
||||
message:
|
||||
requiredService === 'gemini'
|
||||
? '此 API Key 无权访问 Gemini 服务'
|
||||
: '此 API Key 无权访问 Claude 服务'
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -175,6 +182,25 @@ async function handleMessagesRequest(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
logger.api('📥 /v1/messages request received', {
|
||||
model: req.body.model || null,
|
||||
forcedVendor,
|
||||
stream: req.body.stream === true
|
||||
})
|
||||
|
||||
dumpAnthropicMessagesRequest(req, {
|
||||
route: '/v1/messages',
|
||||
forcedVendor,
|
||||
model: req.body?.model || null,
|
||||
stream: req.body?.stream === true
|
||||
})
|
||||
|
||||
// /v1/messages 的扩展:按路径强制分流到 Gemini OAuth 账户(避免 model 前缀混乱)
|
||||
if (forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity') {
|
||||
const baseModel = (req.body.model || '').trim()
|
||||
return await handleAnthropicMessagesToGemini(req, res, { vendor: forcedVendor, baseModel })
|
||||
}
|
||||
|
||||
// 检查是否为流式请求
|
||||
const isStream = req.body.stream === true
|
||||
|
||||
@@ -390,11 +416,18 @@ async function handleMessagesRequest(req, res) {
|
||||
// 根据账号类型选择对应的转发服务并调用
|
||||
if (accountType === 'claude-official') {
|
||||
// 官方Claude账号使用原有的转发服务(会自己选择账号)
|
||||
// 🧹 内存优化:提取需要的值,避免闭包捕获整个 req 对象
|
||||
const _apiKeyId = req.apiKey.id
|
||||
const _rateLimitInfo = req.rateLimitInfo
|
||||
const _requestBody = req.body // 传递后清除引用
|
||||
const _apiKey = req.apiKey
|
||||
const _headers = req.headers
|
||||
|
||||
await claudeRelayService.relayStreamRequestWithUsageCapture(
|
||||
req.body,
|
||||
req.apiKey,
|
||||
_requestBody,
|
||||
_apiKey,
|
||||
res,
|
||||
req.headers,
|
||||
_headers,
|
||||
(usageData) => {
|
||||
// 回调函数:当检测到完整usage数据时记录真实token使用量
|
||||
logger.info(
|
||||
@@ -444,13 +477,13 @@ async function handleMessagesRequest(req, res) {
|
||||
}
|
||||
|
||||
apiKeyService
|
||||
.recordUsageWithDetails(req.apiKey.id, usageObject, model, usageAccountId, 'claude')
|
||||
.recordUsageWithDetails(_apiKeyId, usageObject, model, usageAccountId, accountType)
|
||||
.catch((error) => {
|
||||
logger.error('❌ Failed to record stream usage:', error)
|
||||
})
|
||||
|
||||
queueRateLimitUpdate(
|
||||
req.rateLimitInfo,
|
||||
_rateLimitInfo,
|
||||
{
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
@@ -458,7 +491,9 @@ async function handleMessagesRequest(req, res) {
|
||||
cacheReadTokens
|
||||
},
|
||||
model,
|
||||
'claude-stream'
|
||||
'claude-stream',
|
||||
_apiKeyId,
|
||||
accountType
|
||||
)
|
||||
|
||||
usageDataCaptured = true
|
||||
@@ -475,11 +510,18 @@ async function handleMessagesRequest(req, res) {
|
||||
)
|
||||
} else if (accountType === 'claude-console') {
|
||||
// Claude Console账号使用Console转发服务(需要传递accountId)
|
||||
// 🧹 内存优化:提取需要的值
|
||||
const _apiKeyIdConsole = req.apiKey.id
|
||||
const _rateLimitInfoConsole = req.rateLimitInfo
|
||||
const _requestBodyConsole = req.body
|
||||
const _apiKeyConsole = req.apiKey
|
||||
const _headersConsole = req.headers
|
||||
|
||||
await claudeConsoleRelayService.relayStreamRequestWithUsageCapture(
|
||||
req.body,
|
||||
req.apiKey,
|
||||
_requestBodyConsole,
|
||||
_apiKeyConsole,
|
||||
res,
|
||||
req.headers,
|
||||
_headersConsole,
|
||||
(usageData) => {
|
||||
// 回调函数:当检测到完整usage数据时记录真实token使用量
|
||||
logger.info(
|
||||
@@ -530,7 +572,7 @@ async function handleMessagesRequest(req, res) {
|
||||
|
||||
apiKeyService
|
||||
.recordUsageWithDetails(
|
||||
req.apiKey.id,
|
||||
_apiKeyIdConsole,
|
||||
usageObject,
|
||||
model,
|
||||
usageAccountId,
|
||||
@@ -541,7 +583,7 @@ async function handleMessagesRequest(req, res) {
|
||||
})
|
||||
|
||||
queueRateLimitUpdate(
|
||||
req.rateLimitInfo,
|
||||
_rateLimitInfoConsole,
|
||||
{
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
@@ -549,7 +591,9 @@ async function handleMessagesRequest(req, res) {
|
||||
cacheReadTokens
|
||||
},
|
||||
model,
|
||||
'claude-console-stream'
|
||||
'claude-console-stream',
|
||||
_apiKeyIdConsole,
|
||||
accountType
|
||||
)
|
||||
|
||||
usageDataCaptured = true
|
||||
@@ -567,6 +611,11 @@ async function handleMessagesRequest(req, res) {
|
||||
)
|
||||
} else if (accountType === 'bedrock') {
|
||||
// Bedrock账号使用Bedrock转发服务
|
||||
// 🧹 内存优化:提取需要的值
|
||||
const _apiKeyIdBedrock = req.apiKey.id
|
||||
const _rateLimitInfoBedrock = req.rateLimitInfo
|
||||
const _requestBodyBedrock = req.body
|
||||
|
||||
try {
|
||||
const bedrockAccountResult = await bedrockAccountService.getAccount(accountId)
|
||||
if (!bedrockAccountResult.success) {
|
||||
@@ -574,7 +623,7 @@ async function handleMessagesRequest(req, res) {
|
||||
}
|
||||
|
||||
const result = await bedrockRelayService.handleStreamRequest(
|
||||
req.body,
|
||||
_requestBodyBedrock,
|
||||
bedrockAccountResult.data,
|
||||
res
|
||||
)
|
||||
@@ -585,13 +634,22 @@ async function handleMessagesRequest(req, res) {
|
||||
const outputTokens = result.usage.output_tokens || 0
|
||||
|
||||
apiKeyService
|
||||
.recordUsage(req.apiKey.id, inputTokens, outputTokens, 0, 0, result.model, accountId)
|
||||
.recordUsage(
|
||||
_apiKeyIdBedrock,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
0,
|
||||
0,
|
||||
result.model,
|
||||
accountId,
|
||||
'bedrock'
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error('❌ Failed to record Bedrock stream usage:', error)
|
||||
})
|
||||
|
||||
queueRateLimitUpdate(
|
||||
req.rateLimitInfo,
|
||||
_rateLimitInfoBedrock,
|
||||
{
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
@@ -599,7 +657,9 @@ async function handleMessagesRequest(req, res) {
|
||||
cacheReadTokens: 0
|
||||
},
|
||||
result.model,
|
||||
'bedrock-stream'
|
||||
'bedrock-stream',
|
||||
_apiKeyIdBedrock,
|
||||
'bedrock'
|
||||
)
|
||||
|
||||
usageDataCaptured = true
|
||||
@@ -616,11 +676,18 @@ async function handleMessagesRequest(req, res) {
|
||||
}
|
||||
} else if (accountType === 'ccr') {
|
||||
// CCR账号使用CCR转发服务(需要传递accountId)
|
||||
// 🧹 内存优化:提取需要的值
|
||||
const _apiKeyIdCcr = req.apiKey.id
|
||||
const _rateLimitInfoCcr = req.rateLimitInfo
|
||||
const _requestBodyCcr = req.body
|
||||
const _apiKeyCcr = req.apiKey
|
||||
const _headersCcr = req.headers
|
||||
|
||||
await ccrRelayService.relayStreamRequestWithUsageCapture(
|
||||
req.body,
|
||||
req.apiKey,
|
||||
_requestBodyCcr,
|
||||
_apiKeyCcr,
|
||||
res,
|
||||
req.headers,
|
||||
_headersCcr,
|
||||
(usageData) => {
|
||||
// 回调函数:当检测到完整usage数据时记录真实token使用量
|
||||
logger.info(
|
||||
@@ -670,13 +737,13 @@ async function handleMessagesRequest(req, res) {
|
||||
}
|
||||
|
||||
apiKeyService
|
||||
.recordUsageWithDetails(req.apiKey.id, usageObject, model, usageAccountId, 'ccr')
|
||||
.recordUsageWithDetails(_apiKeyIdCcr, usageObject, model, usageAccountId, 'ccr')
|
||||
.catch((error) => {
|
||||
logger.error('❌ Failed to record CCR stream usage:', error)
|
||||
})
|
||||
|
||||
queueRateLimitUpdate(
|
||||
req.rateLimitInfo,
|
||||
_rateLimitInfoCcr,
|
||||
{
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
@@ -684,7 +751,9 @@ async function handleMessagesRequest(req, res) {
|
||||
cacheReadTokens
|
||||
},
|
||||
model,
|
||||
'ccr-stream'
|
||||
'ccr-stream',
|
||||
_apiKeyIdCcr,
|
||||
'ccr'
|
||||
)
|
||||
|
||||
usageDataCaptured = true
|
||||
@@ -711,18 +780,26 @@ async function handleMessagesRequest(req, res) {
|
||||
}
|
||||
}, 1000) // 1秒后检查
|
||||
} else {
|
||||
// 🧹 内存优化:提取需要的值,避免后续回调捕获整个 req
|
||||
const _apiKeyIdNonStream = req.apiKey.id
|
||||
const _apiKeyNameNonStream = req.apiKey.name
|
||||
const _rateLimitInfoNonStream = req.rateLimitInfo
|
||||
const _requestBodyNonStream = req.body
|
||||
const _apiKeyNonStream = req.apiKey
|
||||
const _headersNonStream = req.headers
|
||||
|
||||
// 🔍 检查客户端连接是否仍然有效(可能在并发排队等待期间断开)
|
||||
if (res.destroyed || res.socket?.destroyed || res.writableEnded) {
|
||||
logger.warn(
|
||||
`⚠️ Client disconnected before non-stream request could start for key: ${req.apiKey?.name || 'unknown'}`
|
||||
`⚠️ Client disconnected before non-stream request could start for key: ${_apiKeyNameNonStream || 'unknown'}`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// 非流式响应 - 只使用官方真实usage数据
|
||||
logger.info('📄 Starting non-streaming request', {
|
||||
apiKeyId: req.apiKey.id,
|
||||
apiKeyName: req.apiKey.name
|
||||
apiKeyId: _apiKeyIdNonStream,
|
||||
apiKeyName: _apiKeyNameNonStream
|
||||
})
|
||||
|
||||
// 📊 监听 socket 事件以追踪连接状态变化
|
||||
@@ -893,11 +970,11 @@ async function handleMessagesRequest(req, res) {
|
||||
? await claudeAccountService.getAccount(accountId)
|
||||
: await claudeConsoleAccountService.getAccount(accountId)
|
||||
|
||||
if (account?.interceptWarmup === 'true' && isWarmupRequest(req.body)) {
|
||||
if (account?.interceptWarmup === 'true' && isWarmupRequest(_requestBodyNonStream)) {
|
||||
logger.api(
|
||||
`🔥 Warmup request intercepted (non-stream) for account: ${account.name} (${accountId})`
|
||||
)
|
||||
return res.json(buildMockWarmupResponse(req.body.model))
|
||||
return res.json(buildMockWarmupResponse(_requestBodyNonStream.model))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -910,11 +987,11 @@ async function handleMessagesRequest(req, res) {
|
||||
if (accountType === 'claude-official') {
|
||||
// 官方Claude账号使用原有的转发服务
|
||||
response = await claudeRelayService.relayRequest(
|
||||
req.body,
|
||||
req.apiKey,
|
||||
req,
|
||||
_requestBodyNonStream,
|
||||
_apiKeyNonStream,
|
||||
req, // clientRequest 用于断开检测,保留但服务层已优化
|
||||
res,
|
||||
req.headers
|
||||
_headersNonStream
|
||||
)
|
||||
} else if (accountType === 'claude-console') {
|
||||
// Claude Console账号使用Console转发服务
|
||||
@@ -922,11 +999,11 @@ async function handleMessagesRequest(req, res) {
|
||||
`[DEBUG] Calling claudeConsoleRelayService.relayRequest with accountId: ${accountId}`
|
||||
)
|
||||
response = await claudeConsoleRelayService.relayRequest(
|
||||
req.body,
|
||||
req.apiKey,
|
||||
req,
|
||||
_requestBodyNonStream,
|
||||
_apiKeyNonStream,
|
||||
req, // clientRequest 保留用于断开检测
|
||||
res,
|
||||
req.headers,
|
||||
_headersNonStream,
|
||||
accountId
|
||||
)
|
||||
} else if (accountType === 'bedrock') {
|
||||
@@ -938,9 +1015,9 @@ async function handleMessagesRequest(req, res) {
|
||||
}
|
||||
|
||||
const result = await bedrockRelayService.handleNonStreamRequest(
|
||||
req.body,
|
||||
_requestBodyNonStream,
|
||||
bedrockAccountResult.data,
|
||||
req.headers
|
||||
_headersNonStream
|
||||
)
|
||||
|
||||
// 构建标准响应格式
|
||||
@@ -970,11 +1047,11 @@ async function handleMessagesRequest(req, res) {
|
||||
// CCR账号使用CCR转发服务
|
||||
logger.debug(`[DEBUG] Calling ccrRelayService.relayRequest with accountId: ${accountId}`)
|
||||
response = await ccrRelayService.relayRequest(
|
||||
req.body,
|
||||
req.apiKey,
|
||||
req,
|
||||
_requestBodyNonStream,
|
||||
_apiKeyNonStream,
|
||||
req, // clientRequest 保留用于断开检测
|
||||
res,
|
||||
req.headers,
|
||||
_headersNonStream,
|
||||
accountId
|
||||
)
|
||||
}
|
||||
@@ -1023,24 +1100,25 @@ async function handleMessagesRequest(req, res) {
|
||||
const cacheCreateTokens = jsonData.usage.cache_creation_input_tokens || 0
|
||||
const cacheReadTokens = jsonData.usage.cache_read_input_tokens || 0
|
||||
// Parse the model to remove vendor prefix if present (e.g., "ccr,gemini-2.5-pro" -> "gemini-2.5-pro")
|
||||
const rawModel = jsonData.model || req.body.model || 'unknown'
|
||||
const { baseModel } = parseVendorPrefixedModel(rawModel)
|
||||
const model = baseModel || rawModel
|
||||
const rawModel = jsonData.model || _requestBodyNonStream.model || 'unknown'
|
||||
const { baseModel: usageBaseModel } = parseVendorPrefixedModel(rawModel)
|
||||
const model = usageBaseModel || rawModel
|
||||
|
||||
// 记录真实的token使用量(包含模型信息和所有4种token以及账户ID)
|
||||
const { accountId: responseAccountId } = response
|
||||
await apiKeyService.recordUsage(
|
||||
req.apiKey.id,
|
||||
_apiKeyIdNonStream,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheCreateTokens,
|
||||
cacheReadTokens,
|
||||
model,
|
||||
responseAccountId
|
||||
responseAccountId,
|
||||
accountType
|
||||
)
|
||||
|
||||
await queueRateLimitUpdate(
|
||||
req.rateLimitInfo,
|
||||
_rateLimitInfoNonStream,
|
||||
{
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
@@ -1048,7 +1126,9 @@ async function handleMessagesRequest(req, res) {
|
||||
cacheReadTokens
|
||||
},
|
||||
model,
|
||||
'claude-non-stream'
|
||||
'claude-non-stream',
|
||||
_apiKeyIdNonStream,
|
||||
accountType
|
||||
)
|
||||
|
||||
usageRecorded = true
|
||||
@@ -1201,6 +1281,65 @@ router.post('/claude/v1/messages', authenticateApiKey, handleMessagesRequest)
|
||||
// 📋 模型列表端点 - 支持 Claude, OpenAI, Gemini
|
||||
router.get('/v1/models', authenticateApiKey, async (req, res) => {
|
||||
try {
|
||||
// Claude Code / Anthropic baseUrl 的分流:/antigravity/api/v1/models 返回 Antigravity 实时模型列表
|
||||
//(通过 v1internal:fetchAvailableModels),避免依赖静态 modelService 列表。
|
||||
const forcedVendor = req._anthropicVendor || null
|
||||
if (forcedVendor === 'antigravity') {
|
||||
if (!apiKeyService.hasPermission(req.apiKey?.permissions, 'gemini')) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
message: '此 API Key 无权访问 Gemini 服务'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unifiedGeminiScheduler = require('../services/unifiedGeminiScheduler')
|
||||
const geminiAccountService = require('../services/geminiAccountService')
|
||||
|
||||
let accountSelection
|
||||
try {
|
||||
accountSelection = await unifiedGeminiScheduler.selectAccountForApiKey(
|
||||
req.apiKey,
|
||||
null,
|
||||
null,
|
||||
{ oauthProvider: 'antigravity' }
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error('Failed to select Gemini OAuth account (antigravity models):', error)
|
||||
return res.status(503).json({ error: 'No available Gemini OAuth accounts' })
|
||||
}
|
||||
|
||||
const account = await geminiAccountService.getAccount(accountSelection.accountId)
|
||||
if (!account) {
|
||||
return res.status(503).json({ error: 'Gemini OAuth account not found' })
|
||||
}
|
||||
|
||||
let proxyConfig = null
|
||||
if (account.proxy) {
|
||||
try {
|
||||
proxyConfig =
|
||||
typeof account.proxy === 'string' ? JSON.parse(account.proxy) : account.proxy
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse proxy configuration:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const models = await geminiAccountService.fetchAvailableModelsAntigravity(
|
||||
account.accessToken,
|
||||
proxyConfig,
|
||||
account.refreshToken
|
||||
)
|
||||
|
||||
// 可选:根据 API Key 的模型限制过滤(黑名单语义)
|
||||
let filteredModels = models
|
||||
if (req.apiKey.enableModelRestriction && req.apiKey.restrictedModels?.length > 0) {
|
||||
filteredModels = models.filter((model) => !req.apiKey.restrictedModels.includes(model.id))
|
||||
}
|
||||
|
||||
return res.json({ object: 'list', data: filteredModels })
|
||||
}
|
||||
|
||||
const modelService = require('../services/modelService')
|
||||
|
||||
// 从 modelService 获取所有支持的模型
|
||||
@@ -1337,20 +1476,27 @@ router.get('/v1/organizations/:org_id/usage', authenticateApiKey, async (req, re
|
||||
|
||||
// 🔢 Token计数端点 - count_tokens beta API
|
||||
router.post('/v1/messages/count_tokens', authenticateApiKey, async (req, res) => {
|
||||
// 检查权限
|
||||
if (
|
||||
req.apiKey.permissions &&
|
||||
req.apiKey.permissions !== 'all' &&
|
||||
req.apiKey.permissions !== 'claude'
|
||||
) {
|
||||
// 按路径强制分流到 Gemini OAuth 账户(避免 model 前缀混乱)
|
||||
const forcedVendor = req._anthropicVendor || null
|
||||
const requiredService =
|
||||
forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity' ? 'gemini' : 'claude'
|
||||
|
||||
if (!apiKeyService.hasPermission(req.apiKey?.permissions, requiredService)) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
type: 'permission_error',
|
||||
message: 'This API key does not have permission to access Claude'
|
||||
message:
|
||||
requiredService === 'gemini'
|
||||
? 'This API key does not have permission to access Gemini'
|
||||
: 'This API key does not have permission to access Claude'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (requiredService === 'gemini') {
|
||||
return await handleAnthropicCountTokensToGemini(req, res, { vendor: forcedVendor })
|
||||
}
|
||||
|
||||
// 🔗 会话绑定验证(与 messages 端点保持一致)
|
||||
const originalSessionId = claudeRelayConfigService.extractOriginalSessionId(req.body)
|
||||
const sessionValidation = await claudeRelayConfigService.validateNewSession(
|
||||
|
||||
Reference in New Issue
Block a user