Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add nodejs commonjs module support #3

Merged
merged 2 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions cjs/fetchSSE.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
};
153 changes: 153 additions & 0 deletions cjs/index.js
Original file line number Diff line number Diff line change
@@ -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;
}
};
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}