CoolFace
Apppublic

lis3456/droid2api

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
routes.js571 linesDownload Raw Back to root
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