import express from 'express'; import fetch from 'node-fetch'; import { getConfig, getModelById, getEndpointByType, getSystemPrompt, getModelReasoning, getRedirectedModelId, getModelProvider } from './config.js'; import { logInfo, logDebug, logError, logRequest, logResponse } from './logger.js'; import { transformToAnthropic, getAnthropicHeaders } from './transformers/request-anthropic.js'; import { transformToOpenAI, getOpenAIHeaders } from './transformers/request-openai.js'; import { transformToCommon, getCommonHeaders } from './transformers/request-common.js'; import { AnthropicResponseTransformer } from './transformers/response-anthropic.js'; import { OpenAIResponseTransformer } from './transformers/response-openai.js'; import { getApiKey } from './auth.js'; import { getNextProxyAgent } from './proxy-manager.js'; import { logRequest as slsLogRequest } from './sls-logger.js'; import { buildDetailedLog } from './log-extractor.js'; const router = express.Router(); /** * Convert a /v1/responses API result to a /v1/chat/completions-compatible format. * Works for non-streaming responses. */ function convertResponseToChatCompletion(resp) { if (!resp || typeof resp !== 'object') { throw new Error('Invalid response object'); } const outputMsg = (resp.output || []).find(o => o.type === 'message'); const textBlocks = outputMsg?.content?.filter(c => c.type === 'output_text') || []; const content = textBlocks.map(c => c.text).join(''); const chatCompletion = { id: resp.id ? resp.id.replace(/^resp_/, 'chatcmpl-') : `chatcmpl-${Date.now()}`, object: 'chat.completion', created: resp.created_at || Math.floor(Date.now() / 1000), model: resp.model || 'unknown-model', choices: [ { index: 0, message: { role: outputMsg?.role || 'assistant', content: content || '' }, finish_reason: resp.status === 'completed' ? 'stop' : 'unknown' } ], usage: { prompt_tokens: resp.usage?.input_tokens ?? 0, completion_tokens: resp.usage?.output_tokens ?? 0, total_tokens: resp.usage?.total_tokens ?? 0 } }; return chatCompletion; } router.get('/v1/models', (req, res) => { logInfo('GET /v1/models'); try { const config = getConfig(); const models = config.models.map(model => ({ id: model.id, object: 'model', created: Date.now(), owned_by: model.type, permission: [], root: model.id, parent: null })); const response = { object: 'list', data: models }; logResponse(200, null, response); res.json(response); } catch (error) { logError('Error in GET /v1/models', error); res.status(500).json({ error: 'Internal server error' }); } }); // 标准 OpenAI 聊天补全处理函数(带格式转换) async function handleChatCompletions(req, res) { logInfo('POST /v1/chat/completions'); const startTime = Date.now(); try { const openaiRequest = req.body; const modelId = getRedirectedModelId(openaiRequest.model); if (!modelId) { return res.status(400).json({ error: 'model is required' }); } const model = getModelById(modelId); if (!model) { return res.status(404).json({ error: `Model ${modelId} not found` }); } const endpoint = getEndpointByType(model.type); if (!endpoint) { return res.status(500).json({ error: `Endpoint type ${model.type} not found` }); } logInfo(`Routing to ${model.type} endpoint: ${endpoint.base_url}`); // Get API key (will auto-refresh if needed) let authHeader; try { authHeader = await getApiKey(req.headers.authorization); } catch (error) { logError('Failed to get API key', error); return res.status(500).json({ error: 'API key not available', message: 'Failed to get or refresh API key. Please check server logs.' }); } let transformedRequest; let headers; const clientHeaders = req.headers; // Log received client headers for debugging logDebug('Client headers received', { 'x-factory-client': clientHeaders['x-factory-client'], 'x-session-id': clientHeaders['x-session-id'], 'x-assistant-message-id': clientHeaders['x-assistant-message-id'], 'user-agent': clientHeaders['user-agent'] }); // Update request body with redirected model ID before transformation const requestWithRedirectedModel = { ...openaiRequest, model: modelId }; // Get provider from model config const provider = getModelProvider(modelId); if (model.type === 'anthropic') { transformedRequest = transformToAnthropic(requestWithRedirectedModel); const isStreaming = openaiRequest.stream === true; headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId, provider); } else if (model.type === 'openai') { transformedRequest = transformToOpenAI(requestWithRedirectedModel); headers = getOpenAIHeaders(authHeader, clientHeaders, provider); } else if (model.type === 'common') { transformedRequest = transformToCommon(requestWithRedirectedModel); headers = getCommonHeaders(authHeader, clientHeaders, provider); } else { return res.status(500).json({ error: `Unknown endpoint type: ${model.type}` }); } logRequest('POST', endpoint.base_url, headers, transformedRequest); const proxyAgentInfo = getNextProxyAgent(endpoint.base_url); const fetchOptions = { method: 'POST', headers, body: JSON.stringify(transformedRequest) }; if (proxyAgentInfo?.agent) { fetchOptions.agent = proxyAgentInfo.agent; } const response = await fetch(endpoint.base_url, fetchOptions); logInfo(`Response status: ${response.status}`); if (!response.ok) { const errorText = await response.text(); logError(`Endpoint error: ${response.status}`, new Error(errorText)); return res.status(response.status).json({ error: `Endpoint returned ${response.status}`, details: errorText }); } const isStreaming = transformedRequest.stream === true; if (isStreaming) { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // common 类型直接转发,不使用 transformer if (model.type === 'common') { try { for await (const chunk of response.body) { res.write(chunk); } res.end(); logInfo('Stream forwarded (common type)'); slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime, req })); } catch (streamError) { logError('Stream error', streamError); res.end(); } } else { // anthropic 和 openai 类型使用 transformer let transformer; if (model.type === 'anthropic') { transformer = new AnthropicResponseTransformer(modelId, `chatcmpl-${Date.now()}`); } else if (model.type === 'openai') { transformer = new OpenAIResponseTransformer(modelId, `chatcmpl-${Date.now()}`); } try { for await (const chunk of transformer.transformStream(response.body)) { res.write(chunk); } res.end(); logInfo('Stream completed'); slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime, req })); } catch (streamError) { logError('Stream error', streamError); res.end(); } } } else { const data = await response.json(); if (model.type === 'openai') { try { const converted = convertResponseToChatCompletion(data); logResponse(200, null, converted); res.json(converted); } catch (e) { // 如果转换失败,回退为原始数据 logResponse(200, null, data); res.json(data); } } else { // anthropic/common: 保持现有逻辑,直接转发 logResponse(200, null, data); res.json(data); } slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime, req, responseData: data })); } } catch (error) { logError('Error in /v1/chat/completions', error); slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/chat/completions', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, req, error: error.message })); res.status(500).json({ error: 'Internal server error', message: error.message }); } } // 直接转发 OpenAI 请求(不做格式转换) async function handleDirectResponses(req, res) { logInfo('POST /v1/responses'); const startTime = Date.now(); try { const openaiRequest = req.body; const modelId = getRedirectedModelId(openaiRequest.model); if (!modelId) { return res.status(400).json({ error: 'model is required' }); } const model = getModelById(modelId); if (!model) { return res.status(404).json({ error: `Model ${modelId} not found` }); } // 只允许 openai 类型端点 if (model.type !== 'openai') { return res.status(400).json({ error: 'Invalid endpoint type', message: `/v1/responses 接口只支持 openai 类型端点,当前模型 ${modelId} 是 ${model.type} 类型` }); } const endpoint = getEndpointByType(model.type); if (!endpoint) { return res.status(500).json({ error: `Endpoint type ${model.type} not found` }); } logInfo(`Direct forwarding to ${model.type} endpoint: ${endpoint.base_url}`); // Get API key - support client x-api-key for anthropic endpoint let authHeader; try { const clientAuthFromXApiKey = req.headers['x-api-key'] ? `Bearer ${req.headers['x-api-key']}` : null; authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey); } catch (error) { logError('Failed to get API key', error); return res.status(500).json({ error: 'API key not available', message: 'Failed to get or refresh API key. Please check server logs.' }); } const clientHeaders = req.headers; // Get provider from model config const provider = getModelProvider(modelId); // 获取 headers const headers = getOpenAIHeaders(authHeader, clientHeaders, provider); // 注入系统提示到 instructions 字段,并更新重定向后的模型ID const systemPrompt = getSystemPrompt(); const modifiedRequest = { ...openaiRequest, model: modelId }; if (systemPrompt) { // 如果已有 instructions,则在前面添加系统提示 if (modifiedRequest.instructions) { modifiedRequest.instructions = systemPrompt + modifiedRequest.instructions; } else { // 否则直接设置系统提示 modifiedRequest.instructions = systemPrompt; } } // 处理reasoning字段 const reasoningLevel = getModelReasoning(modelId); if (reasoningLevel === 'auto') { // Auto模式:保持原始请求的reasoning字段不变 // 如果原始请求有reasoning字段就保留,没有就不添加 } else if (reasoningLevel && ['low', 'medium', 'high', 'xhigh'].includes(reasoningLevel)) { modifiedRequest.reasoning = { effort: reasoningLevel, summary: 'auto' }; } else { // 如果配置是off或无效,移除reasoning字段 delete modifiedRequest.reasoning; } // 删除 claude-cli 特有字段,避免 Factory API 返回 403 delete modifiedRequest.context_management; // 过滤 Claude Code 特有的 MCP 工具 if (modifiedRequest.tools && Array.isArray(modifiedRequest.tools)) { modifiedRequest.tools = modifiedRequest.tools.filter(tool => { if (!tool.name) return true; // 过滤 Claude Code 特有工具 const claudeCodeTools = [ 'Skill', 'EnterPlanMode', 'ExitPlanMode', 'AskUserQuestion', 'TodoWrite' ]; if (claudeCodeTools.includes(tool.name)) return false; // 过滤所有 mcp__ 开头的工具和 MCP 相关工具 return !tool.name.startsWith('mcp__') && !tool.name.includes('Mcp') && !tool.name.includes('MCP'); }); } logRequest('POST', endpoint.base_url, headers, modifiedRequest); // 转发修改后的请求 const proxyAgentInfo = getNextProxyAgent(endpoint.base_url); const fetchOptions = { method: 'POST', headers, body: JSON.stringify(modifiedRequest) }; if (proxyAgentInfo?.agent) { fetchOptions.agent = proxyAgentInfo.agent; } const response = await fetch(endpoint.base_url, fetchOptions); logInfo(`Response status: ${response.status}`); if (!response.ok) { const errorText = await response.text(); logError(`Endpoint error: ${response.status}`, new Error(errorText)); return res.status(response.status).json({ error: `Endpoint returned ${response.status}`, details: errorText }); } const isStreaming = openaiRequest.stream === true; if (isStreaming) { // 直接转发流式响应,不做任何转换 res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); try { // 直接将原始响应流转发给客户端 for await (const chunk of response.body) { res.write(chunk); } res.end(); logInfo('Stream forwarded successfully'); slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/responses', model: modelId, status: 200, duration_ms: Date.now() - startTime, req })); } catch (streamError) { logError('Stream error', streamError); res.end(); } } else { // 直接转发非流式响应,不做任何转换 const data = await response.json(); logResponse(200, null, data); res.json(data); slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/responses', model: modelId, status: 200, duration_ms: Date.now() - startTime, req, responseData: data })); } } catch (error) { logError('Error in /v1/responses', error); slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/responses', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, req, error: error.message })); res.status(500).json({ error: 'Internal server error', message: error.message }); } } // 直接转发 Anthropic 请求(不做格式转换) async function handleDirectMessages(req, res) { logInfo('POST /v1/messages'); const startTime = Date.now(); try { const anthropicRequest = req.body; const modelId = getRedirectedModelId(anthropicRequest.model); if (!modelId) { return res.status(400).json({ error: 'model is required' }); } const model = getModelById(modelId); if (!model) { return res.status(404).json({ error: `Model ${modelId} not found` }); } // 只允许 anthropic 类型端点 if (model.type !== 'anthropic') { return res.status(400).json({ error: 'Invalid endpoint type', message: `/v1/messages 接口只支持 anthropic 类型端点,当前模型 ${modelId} 是 ${model.type} 类型` }); } const endpoint = getEndpointByType(model.type); if (!endpoint) { return res.status(500).json({ error: `Endpoint type ${model.type} not found` }); } logInfo(`Direct forwarding to ${model.type} endpoint: ${endpoint.base_url}`); // Get API key - support client x-api-key for anthropic endpoint let authHeader; try { const clientAuthFromXApiKey = req.headers['x-api-key'] ? `Bearer ${req.headers['x-api-key']}` : null; authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey); } catch (error) { logError('Failed to get API key', error); return res.status(500).json({ error: 'API key not available', message: 'Failed to get or refresh API key. Please check server logs.' }); } const clientHeaders = req.headers; // Get provider from model config const provider = getModelProvider(modelId); // 获取 headers const isStreaming = anthropicRequest.stream === true; const headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId, provider); // 注入系统提示到 system 字段,并更新重定向后的模型ID const systemPrompt = getSystemPrompt(); const modifiedRequest = { ...anthropicRequest, model: modelId }; if (systemPrompt) { if (modifiedRequest.system && Array.isArray(modifiedRequest.system)) { // 如果已有 system 数组,则在最前面插入系统提示 modifiedRequest.system = [ { type: 'text', text: systemPrompt }, ...modifiedRequest.system ]; } else { // 否则创建新的 system 数组 modifiedRequest.system = [ { type: 'text', text: systemPrompt } ]; } } // 将 Claude Code 格式转换为 Droid 格式 (修复 403 错误) if (modifiedRequest.system && Array.isArray(modifiedRequest.system)) { modifiedRequest.system = modifiedRequest.system.map((item, index) => { const newItem = { ...item }; // 删除所有 cache_control if (newItem.cache_control) { delete newItem.cache_control; } // 过滤所有 system 项中的敏感词 if (newItem.text && typeof newItem.text === 'string') { // 替换敏感词以避免 403 错误 newItem.text = newItem.text .replace(/Claude Code/g, 'AI Assistant') .replace(/claude code/g, 'AI assistant') .replace(/\bClaude\b/g, 'AI') .replace(/\bclaude\b/g, 'AI') .replace(/Anthropic/g, 'Factory') .replace(/anthropic/g, 'factory'); } return newItem; }); } // 处理thinking字段 const reasoningLevel = getModelReasoning(modelId); if (reasoningLevel === 'auto') { // Auto模式:保持原始请求的thinking字段不变 // 如果原始请求有thinking字段就保留,没有就不添加 } else if (reasoningLevel && ['low', 'medium', 'high', 'xhigh'].includes(reasoningLevel)) { const budgetTokens = { 'low': 4096, 'medium': 12288, 'high': 24576, 'xhigh': 40960 }; modifiedRequest.thinking = { type: 'enabled', budget_tokens: budgetTokens[reasoningLevel] }; } else { // 如果配置是off或无效,移除thinking字段 delete modifiedRequest.thinking; } // 过滤 messages 中的 Claude Code 特有标识 if (modifiedRequest.messages && Array.isArray(modifiedRequest.messages)) { modifiedRequest.messages = modifiedRequest.messages.map(msg => { if (msg.content && Array.isArray(msg.content)) { msg.content = msg.content.filter(item => { if (item.type === 'text' && item.text) { // 过滤包含 Claude Code 特征的内容 const claudeCodePatterns = [ '', '# claudeMd', '/Users/', '/.claude/', 'CLAUDE.md', '', '', '', 'CodeX MCP', 'codex MCP' ]; return !claudeCodePatterns.some(pattern => item.text.includes(pattern)); } return true; }); } return msg; }); } // 删除 claude-cli 特有字段,避免 Factory API 返回 403 delete modifiedRequest.context_management; // 过滤 Claude Code 特有的 MCP 工具 if (modifiedRequest.tools && Array.isArray(modifiedRequest.tools)) { modifiedRequest.tools = modifiedRequest.tools.filter(tool => { if (!tool.name) return true; // 过滤 Claude Code 特有工具 const claudeCodeTools = [ 'Skill', 'EnterPlanMode', 'ExitPlanMode', 'AskUserQuestion', 'TodoWrite' ]; if (claudeCodeTools.includes(tool.name)) return false; // 过滤所有 mcp__ 开头的工具和 MCP 相关工具 return !tool.name.startsWith('mcp__') && !tool.name.includes('Mcp') && !tool.name.includes('MCP'); }); } logRequest('POST', endpoint.base_url, headers, modifiedRequest); // 转发修改后的请求 const proxyAgentInfo = getNextProxyAgent(endpoint.base_url); const fetchOptions = { method: 'POST', headers, body: JSON.stringify(modifiedRequest) }; if (proxyAgentInfo?.agent) { fetchOptions.agent = proxyAgentInfo.agent; } const response = await fetch(endpoint.base_url, fetchOptions); logInfo(`Response status: ${response.status}`); if (!response.ok) { const errorText = await response.text(); logError(`Endpoint error: ${response.status}`, new Error(errorText)); return res.status(response.status).json({ error: `Endpoint returned ${response.status}`, details: errorText }); } if (isStreaming) { // 直接转发流式响应,不做任何转换 res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); try { // 直接将原始响应流转发给客户端 for await (const chunk of response.body) { res.write(chunk); } res.end(); logInfo('Stream forwarded successfully'); slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/messages', model: modelId, status: 200, duration_ms: Date.now() - startTime, req })); } catch (streamError) { logError('Stream error', streamError); res.end(); } } else { // 直接转发非流式响应,不做任何转换 const data = await response.json(); logResponse(200, null, data); res.json(data); slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/messages', model: modelId, status: 200, duration_ms: Date.now() - startTime, req, responseData: data })); } } catch (error) { logError('Error in /v1/messages', error); slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/messages', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, req, error: error.message })); res.status(500).json({ error: 'Internal server error', message: error.message }); } } // 处理 Anthropic count_tokens 请求 async function handleCountTokens(req, res) { logInfo('POST /v1/messages/count_tokens'); const startTime = Date.now(); try { const anthropicRequest = req.body; const modelId = getRedirectedModelId(anthropicRequest.model); if (!modelId) { return res.status(400).json({ error: 'model is required' }); } const model = getModelById(modelId); if (!model) { return res.status(404).json({ error: `Model ${modelId} not found` }); } // 只允许 anthropic 类型端点 if (model.type !== 'anthropic') { return res.status(400).json({ error: 'Invalid endpoint type', message: `/v1/messages/count_tokens 接口只支持 anthropic 类型端点,当前模型 ${modelId} 是 ${model.type} 类型` }); } const endpoint = getEndpointByType('anthropic'); if (!endpoint) { return res.status(500).json({ error: 'Endpoint type anthropic not found' }); } // Get API key let authHeader; try { const clientAuthFromXApiKey = req.headers['x-api-key'] ? `Bearer ${req.headers['x-api-key']}` : null; authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey); } catch (error) { logError('Failed to get API key', error); return res.status(500).json({ error: 'API key not available', message: 'Failed to get or refresh API key. Please check server logs.' }); } const clientHeaders = req.headers; // Get provider from model config const provider = getModelProvider(modelId); const headers = getAnthropicHeaders(authHeader, clientHeaders, false, modelId, provider); // 构建 count_tokens 端点 URL const countTokensUrl = endpoint.base_url.replace('/v1/messages', '/v1/messages/count_tokens'); // 更新请求体中的模型ID为重定向后的ID const modifiedRequest = { ...anthropicRequest, model: modelId }; logInfo(`Forwarding to count_tokens endpoint: ${countTokensUrl}`); logRequest('POST', countTokensUrl, headers, modifiedRequest); const proxyAgentInfo = getNextProxyAgent(countTokensUrl); const fetchOptions = { method: 'POST', headers, body: JSON.stringify(modifiedRequest) }; if (proxyAgentInfo?.agent) { fetchOptions.agent = proxyAgentInfo.agent; } const response = await fetch(countTokensUrl, fetchOptions); logInfo(`Response status: ${response.status}`); if (!response.ok) { const errorText = await response.text(); logError(`Count tokens error: ${response.status}`, new Error(errorText)); return res.status(response.status).json({ error: `Endpoint returned ${response.status}`, details: errorText }); } const data = await response.json(); logResponse(200, null, data); res.json(data); slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/messages/count_tokens', model: modelId, status: 200, duration_ms: Date.now() - startTime, req, responseData: data })); } catch (error) { logError('Error in /v1/messages/count_tokens', error); slsLogRequest(buildDetailedLog({ method: 'POST', endpoint: '/v1/messages/count_tokens', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, req, error: error.message })); res.status(500).json({ error: 'Internal server error', message: error.message }); } } // 注册路由 router.post('/v1/chat/completions', handleChatCompletions); router.post('/v1/responses', handleDirectResponses); router.post('/v1/messages', handleDirectMessages); router.post('/v1/messages/count_tokens', handleCountTokens); export default router;