Spaces:
Running
Running
File size: 8,036 Bytes
f120063 4289eb1 f120063 4289eb1 f120063 4289eb1 f120063 4289eb1 f120063 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | const axios = require('axios')
const { logger } = require('../utils/logger')
const { getProxyAgent, getCliBaseUrl, applyProxyToAxiosConfig } = require('../utils/proxy-helper')
const usageStats = require('../utils/usage-stats')
const MODEL_REDIRECT = {
'qwen3.5-plus': 'coder-model',
}
function preprocessCliRequestBody(rawBody) {
const body = rawBody && typeof rawBody === 'object' ? JSON.parse(JSON.stringify(rawBody)) : {}
if (body.model && MODEL_REDIRECT[body.model]) {
body.model = MODEL_REDIRECT[body.model]
}
const isStream = body.stream === true
if (isStream) {
const hasToolsArray = Array.isArray(body.tools)
if (!hasToolsArray || body.tools.length === 0) {
body.tools = [{
type: 'function',
function: {
name: 'do_not_call_me',
description: 'Do not call this tool.',
parameters: {
type: 'object',
properties: {
operation: { type: 'number', description: 'placeholder' }
},
required: ['operation']
}
}
}]
}
if (!body.stream_options || typeof body.stream_options !== 'object') {
body.stream_options = {}
}
body.stream_options.include_usage = true
}
return body
}
function formatCliJsonResponse(data, fallbackModel) {
if (!data || typeof data !== 'object') {
return data
}
if (!data.object) {
data.object = 'chat.completion'
}
if (!data.model && fallbackModel) {
data.model = fallbackModel
}
if (!Array.isArray(data.choices)) {
data.choices = []
}
return data
}
/**
* 处理CLI聊天完成请求(支持OpenAI格式的流式和JSON响应)
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
*/
const handleCliChatCompletion = async (req, res) => {
try {
const access_token = req.account.cli_info.access_token
const body = preprocessCliRequestBody(req.body)
const isStream = body.stream === true
// 打印当前使用的账号邮箱
logger.info(`CLI请求使用账号[${req.account.email}]开始处理`, 'CLI', '🚀')
// 无论成功失败都增加请求计数
req.account.cli_info.request_number++
const cliBaseUrl = getCliBaseUrl()
const proxyAgent = getProxyAgent()
// 设置请求配置
const axiosConfig = {
method: 'POST',
url: `${cliBaseUrl}/v1/chat/completions`,
headers: {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/json',
'Accept': isStream ? 'text/event-stream' : 'application/json',
'User-Agent': 'QwenCode/0.10.3 (darwin; arm64)',
'X-Dashscope-Useragent': 'QwenCode/0.10.3 (darwin; arm64)',
'X-Stainless-Runtime-Version': 'v22.17.0',
'Sec-Fetch-Mode': 'cors',
'X-Stainless-Lang': 'js',
'X-Stainless-Arch': 'arm64',
'X-Stainless-Package-Version': '5.11.0',
'X-Dashscope-Cachecontrol': 'enable',
'X-Stainless-Retry-Count': '0',
'X-Stainless-Os': 'MacOS',
'X-Dashscope-Authtype': 'qwen-oauth',
'X-Stainless-Runtime': 'node'
},
data: body,
timeout: 5 * 60 * 1000,
validateStatus: function () {
return true
}
}
// 添加代理配置
if (proxyAgent) {
axiosConfig.httpsAgent = proxyAgent
axiosConfig.proxy = false
}
// 如果是流式请求,设置响应类型为流
if (isStream) {
axiosConfig.responseType = 'stream'
// 设置响应头为流式
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', '*')
}
const response = await axios(axiosConfig)
// 检查响应状态
if (response.status !== 200) {
logger.error(`CLI请求使用账号[${req.account.email}]转发失败 - 状态码: ${response.status} - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI', '❌')
return res.status(response.status).json({
error: {
message: `api_error`,
type: 'api_error',
code: response.status,
details: response.data
}
})
}
// 处理流式响应
if (isStream) {
// 逐行转发,确保始终输出标准 SSE 片段
response.data.on('data', (chunk) => {
const text = chunk.toString('utf8')
const lines = text.split('\n')
for (const line of lines) {
if (!line || !line.startsWith('data:')) continue
res.write(`${line}\n\n`)
}
})
// 处理流错误
response.data.on('error', (streamError) => {
logger.error(`CLI请求使用账号[${req.account.email}]流式传输失败 - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI', '❌')
if (!res.headersSent) {
res.status(500).json({
error: {
message: 'stream_error',
type: 'stream_error',
code: 500
}
})
}
})
// 处理流结束
response.data.on('end', () => {
logger.success(`CLI请求使用账号[${req.account.email}]转发成功 (流式) - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI')
res.end()
usageStats.track({ model: body.model, success: true, usage: { total_tokens: 0 } })
})
} else {
// 处理JSON响应
res.json(formatCliJsonResponse(response.data, body.model))
logger.success(`CLI请求使用账号[${req.account.email}]转发成功 (JSON) - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI')
await usageStats.track({ model: body.model, success: true, usage: response.data?.usage || { total_tokens: 0 } })
}
} catch (error) {
logger.error(`CLI请求使用账号[${req.account.email}]处理异常 - 当前请求数: ${req.account.cli_info.request_number}`, 'CLI', '💥', error.message)
await usageStats.track({ model: req.body?.model, success: false, usage: { total_tokens: 0 } })
// 如果是axios错误,提供更详细的错误信息
if (error.response) {
return res.status(error.response.status).json({
error: {
message: "api_error",
type: 'api_error',
code: error.response.status,
details: error.response.data
}
})
} else if (error.request) {
return res.status(503).json({
error: {
message: 'connection_error',
type: 'connection_error',
code: 503
}
})
} else {
return res.status(500).json({
error: {
message: 'internal_error',
type: 'internal_error',
code: 500
}
})
}
}
}
module.exports = {
handleCliChatCompletion
}
|