From aa383f1d9a563eaef75bd6bfbfe77901121ed15b Mon Sep 17 00:00:00 2001 From: chends Date: Thu, 6 Apr 2023 14:16:36 +0800 Subject: [PATCH 1/2] feat: add nodejs commonjs module support --- cjs/index.js | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 7 ++- 2 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 cjs/index.js diff --git a/cjs/index.js b/cjs/index.js new file mode 100644 index 0000000..e08691b --- /dev/null +++ b/cjs/index.js @@ -0,0 +1,152 @@ +const fetchEventSource = require('@microsoft/fetch-event-source'); +const fetch = require('node-fetch'); + +module.exports = class Api2d { + // 设置key和apiBaseUrl + constructor(key = null, apiBaseUrl = null, timeout = 60000) { + this.key = key; + this.apiBaseUrl = apiBaseUrl || (key && key.startsWith('fk') ? 'https://stream.api2d.net' : 'https://api.openai.com'); + this.timeout = timeout; + this.controller = new AbortController(); + } + + // set key + setKey(key) { + this.key = key; + } + + // set apiBaseUrl + setApiBaseUrl(apiBaseUrl) { + this.apiBaseUrl = apiBaseUrl; + } + + setTimeout(timeout) { + this.timeout = parseInt(timeout) || 60 * 1000; + } + + abort() { + this.controller.abort(); + } + + // Completion + async completion(options) { + // 拼接目标URL + const url = this.apiBaseUrl + '/v1/chat/completions'; + // 拼接headers + const headers = { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.key + }; + + const { onMessage, onEnd, model, ...restOptions } = options; + + // 如果是流式返回,且有回调函数 + if (restOptions.stream && onMessage) { + // 返回一个 Promise + return new Promise(async (resolve, reject) => { + try { + let chars = ''; + console.log('in stream'); + // 使用 fetchEventSource 发送请求 + const timeout_handle = setTimeout(() => { + this.controller.abort(); + // throw new Error( "Timeout "+ this.timeout ); + reject(new Error(`[408]:Timeout by ${this.timeout} ms`)); + }, this.timeout); + const response = await fetchEventSource(url, { + signal: this.controller.signal, + method: 'POST', + headers: { ...headers, Accept: 'text/event-stream' }, + body: JSON.stringify({ ...restOptions, model: model || 'gpt-3.5-turbo' }), + async onopen(response) { + if (response.status != 200) { + throw new Error(`[${response.status}]:${response.statusText}`); + } + }, + onmessage: (e) => { + if (timeout_handle) { + clearTimeout(timeout_handle); + } + if (e.data == '[DONE]') { + // console.log( 'DONE' ); + if (onEnd) onEnd(chars); + resolve(chars); + } else { + // console.log( e.data ); + const event = JSON.parse(e.data); + if (event.choices[0].delta.content) chars += event.choices[0].delta.content; + if (onMessage) onMessage(chars); + } + }, + onerror: (error) => { + console.log(error); + throw new Error(String(error)?.match(/\[(\d+)\]/)?.[1] ? error : `[500]:${error}`); + } + }); + + // const ret = await response.json(); + } catch (error) { + console.log(error); + reject(error); + } + }); + } else { + // 使用 fetch 发送请求 + const response = await fetch(url, { + signal: this.controller.signal, + method: 'POST', + headers: headers, + body: JSON.stringify({ ...restOptions, model: model || 'gpt-3.5-turbo' }) + }); + const timeout_handle = setTimeout(() => { + this.controller.abort(); + }, this.timeout); + const ret = await response.json(); + clearTimeout(timeout_handle); + return ret; + } + } + + async embeddings(options) { + // 拼接目标URL + const url = this.apiBaseUrl + '/v1/embeddings'; + // 拼接headers + const headers = { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.key + }; + const { model, ...restOptions } = options; + // 使用 fetch 发送请求 + const response = await fetch(url, { + signal: this.controller.signal, + method: 'POST', + headers: headers, + body: JSON.stringify({ ...restOptions, model: model || 'text-embedding-ada-002' }) + }); + const timeout_handle = setTimeout(() => { + this.controller.abort(); + }, this.timeout); + const ret = await response.json(); + clearTimeout(timeout_handle); + return ret; + } + + async billing() { + const url = this.apiBaseUrl + '/dashboard/billing/credit_grants'; + const headers = { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + this.key + }; + const response = await fetch(url, { + signal: this.controller.signal, + method: 'GET', + headers: headers + }); + const timeout_handle = setTimeout(() => { + this.controller.abort(); + }, this.timeout); + const ret = await response.json(); + clearTimeout(timeout_handle); + return ret; + } +}; diff --git a/package.json b/package.json index 14be9ab..3380ef0 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,15 @@ "name": "api2d", "version": "0.1.11", "description": "pure browser sdk for api2d and openai", - "main": "index.js", + "main": "cjs/index.js", + "module": "index.js", + "types": "index.d.ts", "repository": "https://github.com/easychen/api2d-js", "author": "EasyChen", "license": "MIT", "private": false, "dependencies": { - "@microsoft/fetch-event-source": "^2.0.1" + "@microsoft/fetch-event-source": "^2.0.1", + "node-fetch": "^2.6.9" } } From 186a151f410e81a8a9585fc5a9aa80b4e90006d4 Mon Sep 17 00:00:00 2001 From: chends Date: Thu, 6 Apr 2023 17:14:45 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dnode=E4=B8=AD?= =?UTF-8?q?=E7=9A=84stream=E6=94=AF=E6=8C=81=EF=BC=8C=E4=BD=BF=E7=94=A8cha?= =?UTF-8?q?tgpt-api=E4=B8=AD=E7=9A=84fetchSSE=E5=AE=9E=E7=8E=B0SSE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetchSSE code by https://github.com/transitive-bullshit/chatgpt-api/blob/600b35eaec985bbbfcb6c77776dc30d4614bd085/src/fetch-sse.ts#L7 --- cjs/fetchSSE.js | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ cjs/index.js | 15 +++++----- package.json | 1 + 3 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 cjs/fetchSSE.js diff --git a/cjs/fetchSSE.js b/cjs/fetchSSE.js new file mode 100644 index 0000000..d1cefd8 --- /dev/null +++ b/cjs/fetchSSE.js @@ -0,0 +1,78 @@ +const { createParser } = require('eventsource-parser'); + +module.exports = async function fetchSSE(url, options, fetch) { + const { onmessage, onError, ...fetchOptions } = options; + const res = await fetch(url, fetchOptions); + if (!res.ok) { + let reason; + + try { + reason = await res.text(); + } catch (err) { + reason = res.statusText; + } + + const msg = `ChatGPT error ${res.status}: ${reason}`; + const error = new Error(msg, { cause: res }); + error.statusCode = res.status; + error.statusText = res.statusText; + throw error; + } + + const parser = createParser((event) => { + if (event.type === 'event') { + onmessage(event.data); + } + }); + + // handle special response errors + const feed = (chunk) => { + let response = null; + + try { + response = JSON.parse(chunk); + } catch { + // ignore + } + + if (response?.detail?.type === 'invalid_request_error') { + const msg = `ChatGPT error ${response.detail.message}: ${response.detail.code} (${response.detail.type})`; + const error = new Error(msg, { cause: response }); + error.statusCode = response.detail.code; + error.statusText = response.detail.message; + + if (onError) { + onError(error); + } else { + console.error(error); + } + + // don't feed to the event parser + return; + } + + parser.feed(chunk); + }; + + if (!res.body.getReader) { + // Vercel polyfills `fetch` with `node-fetch`, which doesn't conform to + // web standards, so this is a workaround... + const body = res.body; + + if (!body.on || !body.read) { + throw new Error('unsupported "fetch" implementation'); + } + + body.on('readable', () => { + let chunk; + while (null !== (chunk = body.read())) { + feed(chunk.toString()); + } + }); + } else { + for await (const chunk of streamAsyncIterable(res.body)) { + const str = new TextDecoder().decode(chunk); + feed(str); + } + } +}; diff --git a/cjs/index.js b/cjs/index.js index e08691b..2a40930 100644 --- a/cjs/index.js +++ b/cjs/index.js @@ -1,4 +1,4 @@ -const fetchEventSource = require('@microsoft/fetch-event-source'); +const fetchSSE = require('./fetchSSE.js'); const fetch = require('node-fetch'); module.exports = class Api2d { @@ -53,9 +53,11 @@ module.exports = class Api2d { // throw new Error( "Timeout "+ this.timeout ); reject(new Error(`[408]:Timeout by ${this.timeout} ms`)); }, this.timeout); - const response = await fetchEventSource(url, { + const response = await fetchSSE(url, { signal: this.controller.signal, method: 'POST', + openWhenHidden: true, + fetch: fetch, headers: { ...headers, Accept: 'text/event-stream' }, body: JSON.stringify({ ...restOptions, model: model || 'gpt-3.5-turbo' }), async onopen(response) { @@ -63,17 +65,16 @@ module.exports = class Api2d { throw new Error(`[${response.status}]:${response.statusText}`); } }, - onmessage: (e) => { + onmessage: (data) => { if (timeout_handle) { clearTimeout(timeout_handle); } - if (e.data == '[DONE]') { + if (data == '[DONE]') { // console.log( 'DONE' ); if (onEnd) onEnd(chars); resolve(chars); } else { - // console.log( e.data ); - const event = JSON.parse(e.data); + const event = JSON.parse(data); if (event.choices[0].delta.content) chars += event.choices[0].delta.content; if (onMessage) onMessage(chars); } @@ -82,7 +83,7 @@ module.exports = class Api2d { console.log(error); throw new Error(String(error)?.match(/\[(\d+)\]/)?.[1] ? error : `[500]:${error}`); } - }); + }, global.fetch || fetch); // const ret = await response.json(); } catch (error) { diff --git a/package.json b/package.json index 3380ef0..38e3709 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "private": false, "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", + "eventsource-parser": "^1.0.0", "node-fetch": "^2.6.9" } }