日常记录

如何利用Node.js和Axios解决混合流和非流响应

最近不断从几个朋友那里听到openai封锁来自大陆地址访问的账号,那么,我这里就实践一个方案,在非大陆地区架设openai的api反向代理,这样使用此代理就可以绕过封锁检查了。

如果您使用过Axios来处理HTTP请求,您可能会遇到这样的问题:某些API端点返回的数据不是常规响应,而是流式响应。但是,大多数Axios教程都假设您将获得常规JSON或XML响应,并且没有涵盖如何处理流响应的情况。这文章正是在解决openai的chat响应形式(server send event, SSE)问题。

在本文中,我们将探讨如何在Node.js中使用Axios处理混合流和非流响应的情况。具体来说,我们将看一下如何:

获取流响应

在处理流响应之前,我们需要了解如何获取它们。 在Axios中,您可以使用responseType选项来指定期望响应类型。 默认情况下,Axios将响应解析为字符串,但是,您可以将其设置为stream以获取流响应。

例如,以下代码将使用Axios从某个API端点获取流响应:

const axios = require('axios');
const fs = require('fs');

const response = await axios.get('https://example.com/stream', {
  responseType: 'stream'
});

response.data.pipe(fs.createWriteStream('./stream.txt'));

在这个例子中,我们向Axios传递了一个responseType选项,用于指定响应的类型为stream。Axios会自动将响应解析为一个可读流,并将其作为data属性返回。我们可以使用.pipe()方法将数据写入文件。

:如果不指定responseType,则没有pipe函数可以调用。

与普通请求混合转发openai的请求。

openai的https://api.openai.com/v1/chat/completions,可以直接get请求,并得到401错误报文,如下案例

{
    "error": {
        "message": "You didn't provide an API key. You need to provide your API key in an Authorization header using Bearer auth (i.e. Authorization: Bearer YOUR_KEY), or as the password field (with blank username) if you're accessing the API from your browser and are prompted for a username and password. You can obtain an API key from https://platform.openai.com/account/api-keys.",
        "type": "invalid_request_error",
        "param": null,
        "code": null
    }
}

但在我的实践中,axios实例调用如果加上了responseType: 'stream',那就错误报错,一直在报网络拒绝,因此需要在代码层面兼容响应式返回。

好在chat的post报文中,有个很明显的stream: true,利用这个字段的值,动态地设置responseType: 'stream',其他情况则使用responseType: 'text',再使用响应报文中是否有content-length字段来判断是否要响应式返回。

最终的代码比较简单

async function transforProxy(req, res, to_url) {
  const headers = req.headers
  const responseType_: ResponseType = req.body.stream ? 'stream' : 'text'
  // request proxy
  axios.request({
    method: req.method,
    url: to_url,
    headers,
    data: req.body,
    responseType: responseType_,
  }).then((response) => {
    res.status(response.status).set(response.headers)
    if (response.headers['content-length'])
      res.send(response.data) // no stream
    else
      response.data.pipe(res) // stream
  }).catch((error) => {
    if (res.headersSent) {
      res.end('close stream')
    }
    else {
      if (error.response) {
        globalThis.console.log(`catch request error ${error}`)
        res.status(error.response.status).set(error.response.headers).end(error.response.data)
      }
      else {
        globalThis.console.error(`catch system error ${error}`)
        res.writeHead(500, { 'Content-Type': 'text/plain' })
        res.end('500 Internal Server Error')
      }
    }
  })
}

router.all('/v1/chat/completions', async (req, res) => {
  const url = `https://api.openai.com${req.url}`
  await transforProxy(req, res, url)
})
Exit mobile version