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 new file mode 100644 index 0000000..2a40930 --- /dev/null +++ b/cjs/index.js @@ -0,0 +1,153 @@ +const fetchSSE = require('./fetchSSE.js'); +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 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) { + if (response.status != 200) { + throw new Error(`[${response.status}]:${response.statusText}`); + } + }, + onmessage: (data) => { + if (timeout_handle) { + clearTimeout(timeout_handle); + } + if (data == '[DONE]') { + // console.log( 'DONE' ); + if (onEnd) onEnd(chars); + resolve(chars); + } else { + const event = JSON.parse(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}`); + } + }, global.fetch || fetch); + + // 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..38e3709 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,16 @@ "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", + "eventsource-parser": "^1.0.0", + "node-fetch": "^2.6.9" } }