From 8aebc63ca00ea43ae0160062b9ea02f69694feca Mon Sep 17 00:00:00 2001 From: weaigc <879821485@qq.com> Date: Mon, 25 Sep 2023 00:07:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=B9=B6=E7=AE=80?= =?UTF-8?q?=E5=8C=96=20OPENAI=20=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OPENAI.md | 87 +++++++++++++++++++ README.md | 1 + package.json | 4 +- server.js | 7 +- src/lib/bots/bing/index.ts | 2 +- src/pages/api/create.ts | 2 - .../api/{v1 => openai}/chat/completions.ts | 22 +++-- tests/openai-stream.ts | 2 +- tests/openai.ts | 2 +- tests/openaipy.py | 9 ++ 10 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 OPENAI.md rename src/pages/api/{v1 => openai}/chat/completions.ts (84%) create mode 100644 tests/openaipy.py diff --git a/OPENAI.md b/OPENAI.md new file mode 100644 index 00000000..9dbde2bd --- /dev/null +++ b/OPENAI.md @@ -0,0 +1,87 @@ +# Bingo OpenAI + +为了方便将 `new bing` 接入到其他 `gpt` 类的项目,现开放 `OpenAI` 格式的 `API` 接口供大家调用。 + +## 接口说明 +### 入参 + * url: /openai/chat/completions (PS: 为了方便兼容不同的项目,所有以 `/completions` 结尾的请求都会被支持) + * Content-Type: application/json + * 参数说明 + * messages 输入的消息列表,完整格式参见 https://platform.openai.com/docs/guides/gpt/chat-completions-api + * model 模型名称(此字段被用于指定 new bing 风格,参数为 Creative、Balanced、Precise 中的一种) + * stream 是否使用流式输出,默认为 + +### 出参 + * Content-Type: application/json 或者 text/event-stream + * 参数说明 + * choices 返回的消息内容,完整格式参见 https://platform.openai.com/docs/guides/gpt/chat-completions-response-format + * id 会话 ID,如需保持上下文,则需要传入此参数,否则会丢失上下文 + +### 示例 +以下以 `curl` 为例 +``` +curl -kL 'https://hf4all-bingo-api.hf.space/api/v1/chat/completions' \ + -H 'Content-Type: application/json' \ + -d '{ + "messages":[{"role":"user","content":"你好"}], + "stream":true, + "model":"Creative" + }' \ +--compressed +``` + +### 限制 + * 暂时只支持聊天(`/completions`)接口,其他接口如有需求,请在 [issue](https://github.com/weaigc/bingo/issues) 提出 + * 受 new bing 限制,暂不支持自定义历史记录 + +## 调用方式 +除了使用 HTTP POST 请求来调用之外,你也可以使用自己熟悉的方式来调用 new bing,如 python 的 openai 库或其它语言的同名包。下面例举一下 Python 和 Node.js 的用法 + +### Python +``` +import openai +openai.api_key = "dummy" +openai.api_base = "https://hf4all-bingo-api.hf.space" # 这里可以改为你自己部署的服务,bingo 服务版本需要 >= 0.9.0 + +# create a chat completion +chat_completion = openai.ChatCompletion.create(model="Creative", messages=[{"role": "user", "content": "Hello"}]) + +# print the completion +print(chat_completion.choices[0].message.content) +``` + +流式输出 +``` + +``` +> 更多使用说明参考 https://github.com/openai/openai-python + +### Node.js +``` +import OpenAI from 'openai'; + +const openai = new OpenAI({ + baseURL: 'https://hf4all-bingo-api.hf.space' // 这里可以改为你自己部署的服务,bingo 服务版本需要 >= 0.9.0 +}); + +async function main() { + const stream = await openai.chat.completions.create({ + model: 'Creative', + messages: [{ role: 'user', content: 'Hello' }], + stream: true, + }); + for await (const part of stream) { + process.stdout.write(part.choices[0]?.delta?.content || ''); + } +} + +main(); +``` +> 更多使用说明参考 https://github.com/openai/openai-node + + +## 在线演示 + +https://huggingface.co/spaces/hf4all/next-web-bing + +[![Deploy to HuggingFace](https://img.shields.io/badge/点此部署-%F0%9F%A4%97-fff)](https://huggingface.co/login?next=%2Fspaces%2Fhf4all%2Fnext-web-bing%3Fduplicate%3Dtrue%26visibility%3Dpublic) 配置可以不改 diff --git a/README.md b/README.md index 4fee185d..0fd401cf 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Bingo,一个让你呼吸顺畅 New Bing。 - 支持持续语音对话 - 支持免账号使用 - 完全免费 +- 支持 OpenAI 方式调用 [使用文档](./OPENAI.md) ## RoadMap diff --git a/package.json b/package.json index cacf3123..94436ca0 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "bingo", - "version": "0.8.0", + "version": "0.9.0", "private": true, "main": "./cloudflare/cli.js", "scripts": { - "dev": "cross-env DEBUG=bingo* next dev --hostname 0.0.0.0", + "dev": "cross-env DEBUG=bingo* NODE_ENV=development node ./server.js", "build": "next build", "start": "cross-env NODE_ENV=production node ./server.js", "lint": "next lint" diff --git a/server.js b/server.js index 6c635681..f9d82fec 100644 --- a/server.js +++ b/server.js @@ -21,15 +21,18 @@ app.prepare().then(() => { async function handleRequest(req, res) { try { + const { method } = req // Be sure to pass `true` as the second argument to `url.parse`. // This tells it to parse the query portion of the URL. const parsedUrl = parse(req.url, true) - const { pathname } = parsedUrl + const { pathname, query } = parsedUrl if (pathname === '/api/counts') { - server.getConnections(function(error, count) { + server.getConnections(function (error, count) { res.end(String(count)) }) + } else if (pathname.endsWith('/completions')) { + await app.render(req, res, '/api/openai/chat/completions', query) } else { await handle(req, res, parsedUrl) } diff --git a/src/lib/bots/bing/index.ts b/src/lib/bots/bing/index.ts index c12bc0d5..20e14f0f 100644 --- a/src/lib/bots/bing/index.ts +++ b/src/lib/bots/bing/index.ts @@ -238,7 +238,7 @@ export class BingWebBot { let resp: ConversationResponse | undefined try { const search = conversationId ? `?conversationId=${encodeURIComponent(conversationId)}` : '' - const response = await fetch(`${this.endpoint}/api/create${search}`, { method: 'POST', headers, redirect: 'error', mode: 'cors', credentials: 'include' }) + const response = await fetch(`${this.endpoint}/api/create${search}`, { method: 'POST', headers, mode: 'cors', credentials: 'include' }) if (response.status === 404) { throw new ChatError('Not Found', ErrorCode.NOTFOUND_ERROR) } diff --git a/src/pages/api/create.ts b/src/pages/api/create.ts index 3be97dc1..a339c3c0 100644 --- a/src/pages/api/create.ts +++ b/src/pages/api/create.ts @@ -32,9 +32,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!json?.conversationSignature) { continue } - const cookies = [`BING_IP=${headers['x-forwarded-for']}`] - res.setHeader('set-cookie', cookies.map(cookie => `${cookie.trim()}; Max-Age=${86400 * 30}; Path=/;`)) debug('headers', headers) res.writeHead(200, { 'Content-Type': 'application/json', diff --git a/src/pages/api/v1/chat/completions.ts b/src/pages/api/openai/chat/completions.ts similarity index 84% rename from src/pages/api/v1/chat/completions.ts rename to src/pages/api/openai/chat/completions.ts index 34f4fb86..56af4f31 100644 --- a/src/pages/api/v1/chat/completions.ts +++ b/src/pages/api/openai/chat/completions.ts @@ -56,22 +56,28 @@ function responseOpenAIMessage(content: string, id?: string): APIResponse { }; } +function getOriginFromHost(host: string) { + const uri = new URL(`http://${host}`) + if (uri.protocol === 'http:' && !/^[0-9.:]+$/.test(host)) { + uri.protocol = 'https:'; + } + return uri.toString().slice(0, -1) +} + export default async function handler(req: NextApiRequest, res: NextApiResponse) { - // if (req.method !== 'POST') return res.status(403).end() + if (req.method === 'GET') return res.status(200).end('ok') await NextCors(req, res, { // Options methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], origin: '*', - optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204 + optionsSuccessStatus: 200, }); const { prompt, stream, model } = parseOpenAIMessage(req.body); - console.log('prompt', prompt, stream, process.env.PORT) let { id } = req.body const chatbot = new BingWebBot({ - endpoint: `http://127.0.0.1:${process.env.PORT}`, - cookie: `BING_IP=${process.env.BING_IP}` + endpoint: getOriginFromHost(req.headers.host || '127.0.0.1:3000'), }) - id ||= JSON.stringify(chatbot.createConversation()) + id ||= JSON.stringify(await chatbot.createConversation()) if (stream) { res.setHeader('Content-Type', 'text/event-stream; charset=utf-8') @@ -96,10 +102,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (stream && lastLength !== lastText.length) { res.write(`data: ${JSON.stringify(responseOpenAIMessage(lastText.slice(lastLength), id))}\n\n`) res.flushHeaders() + lastLength = lastText.length } } else if (event.type === 'ERROR') { - res.write(`data: ${JSON.stringify(responseOpenAIMessage(`${event.error}`, id))}\n\n`) + res.write(`data: ${JSON.stringify(responseOpenAIMessage(`\n\n${event.error}`, id))}\n\n`) + lastText += '\n\n' + event.error res.flushHeaders() } else if (event.type === 'DONE') { if (stream) { diff --git a/tests/openai-stream.ts b/tests/openai-stream.ts index 70ecd340..e27c413b 100644 --- a/tests/openai-stream.ts +++ b/tests/openai-stream.ts @@ -2,7 +2,7 @@ import OpenAI from 'openai'; const openai = new OpenAI({ apiKey: 'dummy', - baseURL: 'https://hf4all-bingo-api.hf.space/api/v1' // 这里改成你自己部署的服务地址 + baseURL: 'http://127.0.0.1:3000' // 这里改成你自己部署的服务地址 }); async function start() { diff --git a/tests/openai.ts b/tests/openai.ts index a6b30154..66bc76c1 100644 --- a/tests/openai.ts +++ b/tests/openai.ts @@ -2,7 +2,7 @@ import OpenAI from 'openai'; const openai = new OpenAI({ apiKey: 'dummy', - baseURL: 'https://hf4all-bingo-api.hf.space/api/v1' // 这里改成你自己部署的服务地址 + baseURL: 'http://127.0.0.1:3000' // 这里改成你自己部署的服务地址 }); async function start() { diff --git a/tests/openaipy.py b/tests/openaipy.py new file mode 100644 index 00000000..6efee312 --- /dev/null +++ b/tests/openaipy.py @@ -0,0 +1,9 @@ +import openai +openai.api_key = "dummy" +openai.api_base = "https://hf4all-bingo-api.hf.space" # 这里可以改为你自己部署的服务,bingo 服务版本需要 >= 0.9.0 + +# create a chat completion +completion = openai.ChatCompletion.create(model="Creative", stream=True, messages=[{"role": "user", "content": "Hello"}]) +for chat_completion in completion: + # print the completion + print(chat_completion.choices[0].message.content, end="", flush=True)