fix: 优化droid转发错误传递

This commit is contained in:
shaw
2025-10-10 20:05:13 +08:00
parent e150bc4beb
commit 6b02dbf040
2 changed files with 146 additions and 44 deletions

View File

@@ -104,41 +104,44 @@ router.post('/claude/v1/messages/count_tokens', authenticateApiKey, async (req,
})
// OpenAI 端点 - /v1/responses
router.post('/openai/v1/responses', authenticateApiKey, async (req, res) => {
try {
const sessionId =
req.headers['session_id'] ||
req.headers['x-session-id'] ||
req.body?.session_id ||
req.body?.conversation_id ||
null
router.post(
['/openai/v1/responses', '/openai/responses'],
authenticateApiKey,
async (req, res) => {
try {
const sessionId =
req.headers['session_id'] ||
req.headers['x-session-id'] ||
req.body?.session_id ||
req.body?.conversation_id ||
null
const sessionHash = sessionId
? crypto.createHash('sha256').update(String(sessionId)).digest('hex')
: null
const sessionHash = sessionId
? crypto.createHash('sha256').update(String(sessionId)).digest('hex')
: null
if (!hasDroidPermission(req.apiKey)) {
logger.security(
`🚫 API Key ${req.apiKey?.id || 'unknown'} 缺少 Droid 权限,拒绝访问 ${req.originalUrl}`
if (!hasDroidPermission(req.apiKey)) {
logger.security(
`🚫 API Key ${req.apiKey?.id || 'unknown'} 缺少 Droid 权限,拒绝访问 ${req.originalUrl}`
)
return res.status(403).json({
error: 'permission_denied',
message: '此 API Key 未启用 Droid 权限'
})
}
const result = await droidRelayService.relayRequest(
req.body,
req.apiKey,
req,
res,
req.headers,
{ endpointType: 'openai', sessionHash }
)
return res.status(403).json({
error: 'permission_denied',
message: '此 API Key 未启用 Droid 权限'
})
}
const result = await droidRelayService.relayRequest(
req.body,
req.apiKey,
req,
res,
req.headers,
{ endpointType: 'openai', sessionHash }
)
if (result.streaming) {
return
}
if (result.streaming) {
return
}
res.status(result.statusCode).set(result.headers).send(result.body)
} catch (error) {
@@ -148,7 +151,8 @@ router.post('/openai/v1/responses', authenticateApiKey, async (req, res) => {
message: error.message
})
}
})
}
)
// 模型列表端点(兼容性)
router.get('/*/v1/models', authenticateApiKey, async (req, res) => {

View File

@@ -16,8 +16,7 @@ const MODEL_REASONING_CONFIG = {
'claude-sonnet-4-20250514': 'medium',
'claude-sonnet-4-5-20250929': 'high',
'gpt-5-2025-08-07': 'high',
'gpt-5-codex': 'off',
'claude-3-5-haiku-20241022': 'off'
'gpt-5-codex': 'off'
}
const VALID_REASONING_LEVELS = new Set(['low', 'medium', 'high'])
@@ -73,6 +72,21 @@ class DroidRelayService {
const normalizedBody = { ...requestBody }
if (endpointType === 'anthropic' && typeof normalizedBody.model === 'string') {
const originalModel = normalizedBody.model
const trimmedModel = originalModel.trim()
const lowerModel = trimmedModel.toLowerCase()
if (lowerModel.includes('haiku')) {
const mappedModel = 'claude-sonnet-4-20250514'
if (originalModel !== mappedModel) {
logger.info(`🔄 将请求模型从 ${originalModel} 映射为 ${mappedModel}`)
}
normalizedBody.model = mappedModel
normalizedBody.__forceDisableThinking = true
}
}
if (endpointType === 'openai' && typeof normalizedBody.model === 'string') {
const originalModel = normalizedBody.model
const trimmedModel = originalModel.trim()
@@ -212,6 +226,7 @@ class DroidRelayService {
// 获取 Factory.ai API URL
let endpointPath = this.endpoints[normalizedEndpoint]
if (typeof customPath === 'string' && customPath.trim()) {
endpointPath = customPath.startsWith('/') ? customPath : `/${customPath}`
}
@@ -313,14 +328,12 @@ class DroidRelayService {
}
}
// 网络错误或其他错误
// 网络错误或其他错误(统一返回 4xx
const mappedStatus = this._mapNetworkErrorStatus(error)
return {
statusCode: 500,
statusCode: mappedStatus,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'relay_error',
message: error.message
})
body: JSON.stringify(this._buildNetworkErrorBody(error))
}
}
}
@@ -393,10 +406,36 @@ class DroidRelayService {
}
logger.error('❌ Droid stream error:', error)
if (!clientResponse.destroyed && !clientResponse.writableEnded) {
clientResponse.end()
const mappedStatus = this._mapNetworkErrorStatus(error)
const errorBody = this._buildNetworkErrorBody(error)
if (!clientResponse.destroyed) {
if (!clientResponse.writableEnded) {
const canUseJson =
!hasForwardedData &&
typeof clientResponse.status === 'function' &&
typeof clientResponse.json === 'function'
if (canUseJson) {
clientResponse.status(mappedStatus).json(errorBody)
} else {
const errorPayload = JSON.stringify(errorBody)
if (!hasForwardedData) {
if (typeof clientResponse.setHeader === 'function') {
clientResponse.setHeader('Content-Type', 'application/json')
}
clientResponse.write(errorPayload)
clientResponse.end()
} else {
clientResponse.write(`event: error\ndata: ${errorPayload}\n\n`)
clientResponse.end()
}
}
}
}
resolveOnce({ statusCode: 500, streaming: true, error })
resolveOnce({ statusCode: mappedStatus, streaming: true, error })
} else {
rejectOnce(error)
}
@@ -852,6 +891,17 @@ class DroidRelayService {
const { disableStreaming = false } = options
const processedBody = { ...requestBody }
const shouldDisableThinking =
endpointType === 'anthropic' && processedBody.__forceDisableThinking === true
if ('__forceDisableThinking' in processedBody) {
delete processedBody.__forceDisableThinking
}
if (requestBody && '__forceDisableThinking' in requestBody) {
delete requestBody.__forceDisableThinking
}
if (processedBody && Object.prototype.hasOwnProperty.call(processedBody, 'metadata')) {
delete processedBody.metadata
}
@@ -880,7 +930,7 @@ class DroidRelayService {
}
}
const reasoningLevel = this._getReasoningLevel(requestBody)
const reasoningLevel = shouldDisableThinking ? null : this._getReasoningLevel(requestBody)
if (reasoningLevel) {
const budgetTokens = {
low: 4096,
@@ -894,6 +944,12 @@ class DroidRelayService {
} else {
delete processedBody.thinking
}
if (shouldDisableThinking) {
if ('thinking' in processedBody) {
delete processedBody.thinking
}
}
}
// OpenAI 端点:处理 reasoning 字段
@@ -1039,6 +1095,48 @@ class DroidRelayService {
}
}
_mapNetworkErrorStatus(error) {
const code = (error && error.code ? String(error.code) : '').toUpperCase()
if (code === 'ECONNABORTED' || code === 'ETIMEDOUT') {
return 408
}
if (code === 'ECONNRESET' || code === 'EPIPE') {
return 424
}
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') {
return 424
}
if (typeof error === 'object' && error !== null) {
const message = (error.message || '').toLowerCase()
if (message.includes('timeout')) {
return 408
}
}
return 424
}
_buildNetworkErrorBody(error) {
const body = {
error: 'relay_upstream_failure',
message: error?.message || '上游请求失败'
}
if (error?.code) {
body.code = error.code
}
if (error?.config?.url) {
body.upstream = error.config.url
}
return body
}
/**
* 生成 UUID
*/