lis3456/droid2api
0
1import express from 'express';2import fetch from 'node-fetch';3import { getConfig, getModelById, getEndpointByType, getSystemPrompt, getModelReasoning } from './config.js';4import { logInfo, logDebug, logError, logRequest, logResponse } from './logger.js';5import { transformToAnthropic, getAnthropicHeaders } from './transformers/request-anthropic.js';6import { transformToOpenAI, getOpenAIHeaders } from './transformers/request-openai.js';7import { transformToCommon, getCommonHeaders } from './transformers/request-common.js';8import { AnthropicResponseTransformer } from './transformers/response-anthropic.js';9import { OpenAIResponseTransformer } from './transformers/response-openai.js';10import { getApiKey } from './auth.js';11import keyPoolManager from './auth.js';12const router = express.Router();13 14/**15 * Convert a /v1/responses API result to a /v1/chat/completions-compatible format.16 * Works for non-streaming responses.17 */18function convertResponseToChatCompletion(resp) {19 if (!resp || typeof resp !== 'object') {20 throw new Error('Invalid response object');21 }22 23 const outputMsg = (resp.output || []).find(o => o.type === 'message');24 const textBlocks = outputMsg?.content?.filter(c => c.type === 'output_text') || [];25 const content = textBlocks.map(c => c.text).join('');26 27 const chatCompletion = {28 id: resp.id ? resp.id.replace(/^resp_/, 'chatcmpl-') : `chatcmpl-${Date.now()}`,29 object: 'chat.completion',30 created: resp.created_at || Math.floor(Date.now() / 1000),31 model: resp.model || 'unknown-model',32 choices: [33 {34 index: 0,35 message: {36 role: outputMsg?.role || 'assistant',37 content: content || ''38 },39 finish_reason: resp.status === 'completed' ? 'stop' : 'unknown'40 }41 ],42 usage: {43 prompt_tokens: resp.usage?.input_tokens ?? 0,44 completion_tokens: resp.usage?.output_tokens ?? 0,45 total_tokens: resp.usage?.total_tokens ?? 046 }47 };48 49 return chatCompletion;50}51 52router.get('/v1/models', (req, res) => {53 logInfo('GET /v1/models');54 55 try {56 const config = getConfig();57 const models = config.models.map(model => ({58 id: model.id,59 object: 'model',60 created: Date.now(),61 owned_by: model.type,62 permission: [],63 root: model.id,64 parent: null65 }));66 67 const response = {68 object: 'list',69 data: models70 };71 72 logResponse(200, null, response);73 res.json(response);74 } catch (error) {75 logError('Error in GET /v1/models', error);76 res.status(500).json({ error: 'Internal server error' });77 }78});79 80// 标准 OpenAI 聊天补全处理函数(带格式转换)81async function handleChatCompletions(req, res) {82 logInfo('POST /v1/chat/completions');83 84 try {85 const openaiRequest = req.body;86 const modelId = openaiRequest.model;87 88 if (!modelId) {89 return res.status(400).json({ error: 'model is required' });90 }91 92 const model = getModelById(modelId);93 if (!model) {94 return res.status(404).json({ error: `Model ${modelId} not found` });95 }96 97 const endpoint = getEndpointByType(model.type);98 if (!endpoint) {99 return res.status(500).json({ error: `Endpoint type ${model.type} not found` });100 }101 102 logInfo(`Routing to ${model.type} endpoint: ${endpoint.base_url}`);103 104 // Get API key from pool (轮询获取)105 let authHeader;106 let currentKeyId;107 try {108 const keyResult = await keyPoolManager.getNextKey();109 authHeader = `Bearer ${keyResult.key}`;110 currentKeyId = keyResult.keyId;111 } catch (error) {112 logError('Failed to get API key from pool', error);113 return res.status(500).json({114 error: '密钥池错误',115 message: error.message116 });117 }118 119 let transformedRequest;120 let headers;121 const clientHeaders = req.headers;122 123 // Log received client headers for debugging124 logDebug('Client headers received', {125 'x-factory-client': clientHeaders['x-factory-client'],126 'x-session-id': clientHeaders['x-session-id'],127 'x-assistant-message-id': clientHeaders['x-assistant-message-id'],128 'user-agent': clientHeaders['user-agent']129 });130 131 if (model.type === 'anthropic') {132 transformedRequest = transformToAnthropic(openaiRequest);133 const isStreaming = openaiRequest.stream === true;134 headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId);135 } else if (model.type === 'openai') {136 transformedRequest = transformToOpenAI(openaiRequest);137 headers = getOpenAIHeaders(authHeader, clientHeaders);138 } else if (model.type === 'common') {139 transformedRequest = transformToCommon(openaiRequest);140 headers = getCommonHeaders(authHeader, clientHeaders);141 } else {142 return res.status(500).json({ error: `Unknown endpoint type: ${model.type}` });143 }144 145 logRequest('POST', endpoint.base_url, headers, transformedRequest);146 147 const response = await fetch(endpoint.base_url, {148 method: 'POST',149 headers,150 body: JSON.stringify(transformedRequest)151 });152 153 logInfo(`Response status: ${response.status}`);154 155 // 处理402错误 - 永久封禁密钥156 if (response.status === 402) {157 keyPoolManager.banKey(currentKeyId, 'Payment Required - No Credits');158 const errorText = await response.text();159 logError(`Key banned due to 402 error: ${currentKeyId}`, new Error(errorText));160 return res.status(402).json({161 error: 'Payment Required',162 message: 'Key has been banned due to insufficient credits',163 details: errorText164 });165 }166 167 if (!response.ok) {168 const errorText = await response.text();169 logError(`Endpoint error: ${response.status}`, new Error(errorText));170 return res.status(response.status).json({171 error: `Endpoint returned ${response.status}`,172 details: errorText173 });174 }175 176 const isStreaming = transformedRequest.stream === true;177 178 if (isStreaming) {179 res.setHeader('Content-Type', 'text/event-stream');180 res.setHeader('Cache-Control', 'no-cache');181 res.setHeader('Connection', 'keep-alive');182 183 // common 类型直接转发,不使用 transformer184 if (model.type === 'common') {185 try {186 for await (const chunk of response.body) {187 res.write(chunk);188 }189 res.end();190 logInfo('Stream forwarded (common type)');191 } catch (streamError) {192 logError('Stream error', streamError);193 res.end();194 }195 } else {196 // anthropic 和 openai 类型使用 transformer197 let transformer;198 if (model.type === 'anthropic') {199 transformer = new AnthropicResponseTransformer(modelId, `chatcmpl-${Date.now()}`);200 } else if (model.type === 'openai') {201 transformer = new OpenAIResponseTransformer(modelId, `chatcmpl-${Date.now()}`);202 }203 204 try {205 for await (const chunk of transformer.transformStream(response.body)) {206 res.write(chunk);207 }208 res.end();209 logInfo('Stream completed');210 } catch (streamError) {211 logError('Stream error', streamError);212 res.end();213 }214 }215 } else {216 const data = await response.json();217 if (model.type === 'openai') {218 try {219 const converted = convertResponseToChatCompletion(data);220 logResponse(200, null, converted);221 res.json(converted);222 } catch (e) {223 // 如果转换失败,回退为原始数据224 logResponse(200, null, data);225 res.json(data);226 }227 } else {228 // anthropic/common: 保持现有逻辑,直接转发229 logResponse(200, null, data);230 res.json(data);231 }232 }233 234 } catch (error) {235 logError('Error in /v1/chat/completions', error);236 // 老王:检查响应是否已经开始发送,避免重复发送导致崩溃237 if (!res.headersSent) {238 res.status(500).json({239 error: 'Internal server error',240 message: error.message241 });242 } else {243 // 流式响应已开始,直接结束连接244 res.end();245 }246 }247}248 249// 直接转发 OpenAI 请求(不做格式转换)250async function handleDirectResponses(req, res) {251 logInfo('POST /v1/responses');252 253 try {254 const openaiRequest = req.body;255 const modelId = openaiRequest.model;256 257 if (!modelId) {258 return res.status(400).json({ error: 'model is required' });259 }260 261 const model = getModelById(modelId);262 if (!model) {263 return res.status(404).json({ error: `Model ${modelId} not found` });264 }265 266 // 只允许 openai 类型端点267 if (model.type !== 'openai') {268 return res.status(400).json({269 error: 'Invalid endpoint type',270 message: `/v1/responses 接口只支持 openai 类型端点,当前模型 ${modelId} 是 ${model.type} 类型`271 });272 }273 274 const endpoint = getEndpointByType(model.type);275 if (!endpoint) {276 return res.status(500).json({ error: `Endpoint type ${model.type} not found` });277 }278 279 logInfo(`Direct forwarding to ${model.type} endpoint: ${endpoint.base_url}`);280 281 // Get API key from pool (轮询获取)282 let authHeader;283 let currentKeyId;284 try {285 const keyResult = await keyPoolManager.getNextKey();286 authHeader = `Bearer ${keyResult.key}`;287 currentKeyId = keyResult.keyId;288 } catch (error) {289 logError('Failed to get API key from pool', error);290 return res.status(500).json({291 error: '密钥池错误',292 message: error.message293 });294 }295 296 const clientHeaders = req.headers;297 298 // 获取 headers299 const headers = getOpenAIHeaders(authHeader, clientHeaders);300 301 // 注入系统提示到 instructions 字段302 const systemPrompt = getSystemPrompt();303 const modifiedRequest = { ...openaiRequest };304 if (systemPrompt) {305 // 如果已有 instructions,则在前面添加系统提示306 if (modifiedRequest.instructions) {307 modifiedRequest.instructions = systemPrompt + modifiedRequest.instructions;308 } else {309 // 否则直接设置系统提示310 modifiedRequest.instructions = systemPrompt;311 }312 }313 314 // 处理reasoning字段315 const reasoningLevel = getModelReasoning(modelId);316 if (reasoningLevel === 'auto') {317 // Auto模式:保持原始请求的reasoning字段不变318 // 如果原始请求有reasoning字段就保留,没有就不添加319 } else if (reasoningLevel && ['low', 'medium', 'high'].includes(reasoningLevel)) {320 modifiedRequest.reasoning = {321 effort: reasoningLevel,322 summary: 'auto'323 };324 } else {325 // 如果配置是off或无效,移除reasoning字段326 delete modifiedRequest.reasoning;327 }328 329 logRequest('POST', endpoint.base_url, headers, modifiedRequest);330 331 // 转发修改后的请求332 const response = await fetch(endpoint.base_url, {333 method: 'POST',334 headers,335 body: JSON.stringify(modifiedRequest)336 });337 338 logInfo(`Response status: ${response.status}`);339 340 // 处理402错误 - 永久封禁密钥341 if (response.status === 402) {342 keyPoolManager.banKey(currentKeyId, 'Payment Required - No Credits');343 const errorText = await response.text();344 logError(`Key banned due to 402 error: ${currentKeyId}`, new Error(errorText));345 return res.status(402).json({346 error: 'Payment Required',347 message: 'Key has been banned due to insufficient credits',348 details: errorText349 });350 }351 352 if (!response.ok) {353 const errorText = await response.text();354 logError(`Endpoint error: ${response.status}`, new Error(errorText));355 return res.status(response.status).json({356 error: `Endpoint returned ${response.status}`,357 details: errorText358 });359 }360 361 const isStreaming = openaiRequest.stream === true;362 363 if (isStreaming) {364 // 直接转发流式响应,不做任何转换365 res.setHeader('Content-Type', 'text/event-stream');366 res.setHeader('Cache-Control', 'no-cache');367 res.setHeader('Connection', 'keep-alive');368 369 try {370 // 直接将原始响应流转发给客户端371 for await (const chunk of response.body) {372 res.write(chunk);373 }374 res.end();375 logInfo('Stream forwarded successfully');376 } catch (streamError) {377 logError('Stream error', streamError);378 res.end();379 }380 } else {381 // 直接转发非流式响应,不做任何转换382 const data = await response.json();383 logResponse(200, null, data);384 res.json(data);385 }386 387 } catch (error) {388 logError('Error in /v1/responses', error);389 // 老王:检查响应是否已经开始发送,避免重复发送导致崩溃390 if (!res.headersSent) {391 res.status(500).json({392 error: 'Internal server error',393 message: error.message394 });395 } else {396 // 流式响应已开始,直接结束连接397 res.end();398 }399 }400}401 402// 直接转发 Anthropic 请求(不做格式转换)403async function handleDirectMessages(req, res) {404 logInfo('POST /v1/messages');405 406 try {407 const anthropicRequest = req.body;408 const modelId = anthropicRequest.model;409 410 if (!modelId) {411 return res.status(400).json({ error: 'model is required' });412 }413 414 const model = getModelById(modelId);415 if (!model) {416 return res.status(404).json({ error: `Model ${modelId} not found` });417 }418 419 // 只允许 anthropic 类型端点420 if (model.type !== 'anthropic') {421 return res.status(400).json({ 422 error: 'Invalid endpoint type',423 message: `/v1/messages 接口只支持 anthropic 类型端点,当前模型 ${modelId} 是 ${model.type} 类型`424 });425 }426 427 const endpoint = getEndpointByType(model.type);428 if (!endpoint) {429 return res.status(500).json({ error: `Endpoint type ${model.type} not found` });430 }431 432 logInfo(`Direct forwarding to ${model.type} endpoint: ${endpoint.base_url}`);433 434 // Get API key from pool (轮询获取)435 let authHeader;436 let currentKeyId;437 try {438 const keyResult = await keyPoolManager.getNextKey();439 authHeader = `Bearer ${keyResult.key}`;440 currentKeyId = keyResult.keyId;441 } catch (error) {442 logError('Failed to get API key from pool', error);443 return res.status(500).json({444 error: '密钥池错误',445 message: error.message446 });447 }448 449 const clientHeaders = req.headers;450 451 // 获取 headers452 const isStreaming = anthropicRequest.stream === true;453 const headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId);454 455 // 注入系统提示到 system 字段456 const systemPrompt = getSystemPrompt();457 const modifiedRequest = { ...anthropicRequest };458 if (systemPrompt) {459 if (modifiedRequest.system && Array.isArray(modifiedRequest.system)) {460 // 如果已有 system 数组,则在最前面插入系统提示461 modifiedRequest.system = [462 { type: 'text', text: systemPrompt },463 ...modifiedRequest.system464 ];465 } else {466 // 否则创建新的 system 数组467 modifiedRequest.system = [468 { type: 'text', text: systemPrompt }469 ];470 }471 }472 473 // 处理thinking字段474 const reasoningLevel = getModelReasoning(modelId);475 if (reasoningLevel === 'auto') {476 // Auto模式:保持原始请求的thinking字段不变477 // 如果原始请求有thinking字段就保留,没有就不添加478 } else if (reasoningLevel && ['low', 'medium', 'high'].includes(reasoningLevel)) {479 const budgetTokens = {480 'low': 4096,481 'medium': 12288,482 'high': 24576483 };484 485 modifiedRequest.thinking = {486 type: 'enabled',487 budget_tokens: budgetTokens[reasoningLevel]488 };489 } else {490 // 如果配置是off或无效,移除thinking字段491 delete modifiedRequest.thinking;492 }493 494 logRequest('POST', endpoint.base_url, headers, modifiedRequest);495 496 // 转发修改后的请求497 const response = await fetch(endpoint.base_url, {498 method: 'POST',499 headers,500 body: JSON.stringify(modifiedRequest)501 });502 503 logInfo(`Response status: ${response.status}`);504 505 // 处理402错误 - 永久封禁密钥506 if (response.status === 402) {507 keyPoolManager.banKey(currentKeyId, 'Payment Required - No Credits');508 const errorText = await response.text();509 logError(`Key banned due to 402 error: ${currentKeyId}`, new Error(errorText));510 return res.status(402).json({511 error: 'Payment Required',512 message: 'Key has been banned due to insufficient credits',513 details: errorText514 });515 }516 517 if (!response.ok) {518 const errorText = await response.text();519 logError(`Endpoint error: ${response.status}`, new Error(errorText));520 return res.status(response.status).json({521 error: `Endpoint returned ${response.status}`,522 details: errorText523 });524 }525 526 if (isStreaming) {527 // 直接转发流式响应,不做任何转换528 res.setHeader('Content-Type', 'text/event-stream');529 res.setHeader('Cache-Control', 'no-cache');530 res.setHeader('Connection', 'keep-alive');531 532 try {533 // 直接将原始响应流转发给客户端534 for await (const chunk of response.body) {535 res.write(chunk);536 }537 res.end();538 logInfo('Stream forwarded successfully');539 } catch (streamError) {540 logError('Stream error', streamError);541 res.end();542 }543 } else {544 // 直接转发非流式响应,不做任何转换545 const data = await response.json();546 logResponse(200, null, data);547 res.json(data);548 }549 550 } catch (error) {551 logError('Error in /v1/messages', error);552 // 老王:检查响应是否已经开始发送,避免重复发送导致崩溃553 if (!res.headersSent) {554 res.status(500).json({555 error: 'Internal server error',556 message: error.message557 });558 } else {559 // 流式响应已开始,直接结束连接560 res.end();561 }562 }563}564 565// 注册路由566router.post('/v1/chat/completions', handleChatCompletions);567router.post('/v1/responses', handleDirectResponses);568router.post('/v1/messages', handleDirectMessages);569 570export default router;571 