diff --git a/OPENAI.md b/OPENAI.md new file mode 100644 index 00000000..66237eaa --- /dev/null +++ b/OPENAI.md @@ -0,0 +1,72 @@ +# 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` 为例 +``` + +``` + +### 限制 + * 暂时只支持聊天(`/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 + + +## 在线演示 + 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 86% rename from src/pages/api/v1/chat/completions.ts rename to src/pages/api/openai/chat/completions.ts index 34f4fb86..22209e0f 100644 --- a/src/pages/api/v1/chat/completions.ts +++ b/src/pages/api/openai/chat/completions.ts @@ -56,20 +56,26 @@ 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 !== 'POST') 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()) @@ -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() {