Skip to content

Commit c5d0ae9

Browse files
authored
Merge pull request #3 from cdswyda/master
feat: add nodejs commonjs module support
2 parents 6c43dbf + 186a151 commit c5d0ae9

File tree

3 files changed

+237
-2
lines changed

3 files changed

+237
-2
lines changed

cjs/fetchSSE.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const { createParser } = require('eventsource-parser');
2+
3+
module.exports = async function fetchSSE(url, options, fetch) {
4+
const { onmessage, onError, ...fetchOptions } = options;
5+
const res = await fetch(url, fetchOptions);
6+
if (!res.ok) {
7+
let reason;
8+
9+
try {
10+
reason = await res.text();
11+
} catch (err) {
12+
reason = res.statusText;
13+
}
14+
15+
const msg = `ChatGPT error ${res.status}: ${reason}`;
16+
const error = new Error(msg, { cause: res });
17+
error.statusCode = res.status;
18+
error.statusText = res.statusText;
19+
throw error;
20+
}
21+
22+
const parser = createParser((event) => {
23+
if (event.type === 'event') {
24+
onmessage(event.data);
25+
}
26+
});
27+
28+
// handle special response errors
29+
const feed = (chunk) => {
30+
let response = null;
31+
32+
try {
33+
response = JSON.parse(chunk);
34+
} catch {
35+
// ignore
36+
}
37+
38+
if (response?.detail?.type === 'invalid_request_error') {
39+
const msg = `ChatGPT error ${response.detail.message}: ${response.detail.code} (${response.detail.type})`;
40+
const error = new Error(msg, { cause: response });
41+
error.statusCode = response.detail.code;
42+
error.statusText = response.detail.message;
43+
44+
if (onError) {
45+
onError(error);
46+
} else {
47+
console.error(error);
48+
}
49+
50+
// don't feed to the event parser
51+
return;
52+
}
53+
54+
parser.feed(chunk);
55+
};
56+
57+
if (!res.body.getReader) {
58+
// Vercel polyfills `fetch` with `node-fetch`, which doesn't conform to
59+
// web standards, so this is a workaround...
60+
const body = res.body;
61+
62+
if (!body.on || !body.read) {
63+
throw new Error('unsupported "fetch" implementation');
64+
}
65+
66+
body.on('readable', () => {
67+
let chunk;
68+
while (null !== (chunk = body.read())) {
69+
feed(chunk.toString());
70+
}
71+
});
72+
} else {
73+
for await (const chunk of streamAsyncIterable(res.body)) {
74+
const str = new TextDecoder().decode(chunk);
75+
feed(str);
76+
}
77+
}
78+
};

cjs/index.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
const fetchSSE = require('./fetchSSE.js');
2+
const fetch = require('node-fetch');
3+
4+
module.exports = class Api2d {
5+
// 设置key和apiBaseUrl
6+
constructor(key = null, apiBaseUrl = null, timeout = 60000) {
7+
this.key = key;
8+
this.apiBaseUrl = apiBaseUrl || (key && key.startsWith('fk') ? 'https://stream.api2d.net' : 'https://api.openai.com');
9+
this.timeout = timeout;
10+
this.controller = new AbortController();
11+
}
12+
13+
// set key
14+
setKey(key) {
15+
this.key = key;
16+
}
17+
18+
// set apiBaseUrl
19+
setApiBaseUrl(apiBaseUrl) {
20+
this.apiBaseUrl = apiBaseUrl;
21+
}
22+
23+
setTimeout(timeout) {
24+
this.timeout = parseInt(timeout) || 60 * 1000;
25+
}
26+
27+
abort() {
28+
this.controller.abort();
29+
}
30+
31+
// Completion
32+
async completion(options) {
33+
// 拼接目标URL
34+
const url = this.apiBaseUrl + '/v1/chat/completions';
35+
// 拼接headers
36+
const headers = {
37+
'Content-Type': 'application/json',
38+
Authorization: 'Bearer ' + this.key
39+
};
40+
41+
const { onMessage, onEnd, model, ...restOptions } = options;
42+
43+
// 如果是流式返回,且有回调函数
44+
if (restOptions.stream && onMessage) {
45+
// 返回一个 Promise
46+
return new Promise(async (resolve, reject) => {
47+
try {
48+
let chars = '';
49+
console.log('in stream');
50+
// 使用 fetchEventSource 发送请求
51+
const timeout_handle = setTimeout(() => {
52+
this.controller.abort();
53+
// throw new Error( "Timeout "+ this.timeout );
54+
reject(new Error(`[408]:Timeout by ${this.timeout} ms`));
55+
}, this.timeout);
56+
const response = await fetchSSE(url, {
57+
signal: this.controller.signal,
58+
method: 'POST',
59+
openWhenHidden: true,
60+
fetch: fetch,
61+
headers: { ...headers, Accept: 'text/event-stream' },
62+
body: JSON.stringify({ ...restOptions, model: model || 'gpt-3.5-turbo' }),
63+
async onopen(response) {
64+
if (response.status != 200) {
65+
throw new Error(`[${response.status}]:${response.statusText}`);
66+
}
67+
},
68+
onmessage: (data) => {
69+
if (timeout_handle) {
70+
clearTimeout(timeout_handle);
71+
}
72+
if (data == '[DONE]') {
73+
// console.log( 'DONE' );
74+
if (onEnd) onEnd(chars);
75+
resolve(chars);
76+
} else {
77+
const event = JSON.parse(data);
78+
if (event.choices[0].delta.content) chars += event.choices[0].delta.content;
79+
if (onMessage) onMessage(chars);
80+
}
81+
},
82+
onerror: (error) => {
83+
console.log(error);
84+
throw new Error(String(error)?.match(/\[(\d+)\]/)?.[1] ? error : `[500]:${error}`);
85+
}
86+
}, global.fetch || fetch);
87+
88+
// const ret = await response.json();
89+
} catch (error) {
90+
console.log(error);
91+
reject(error);
92+
}
93+
});
94+
} else {
95+
// 使用 fetch 发送请求
96+
const response = await fetch(url, {
97+
signal: this.controller.signal,
98+
method: 'POST',
99+
headers: headers,
100+
body: JSON.stringify({ ...restOptions, model: model || 'gpt-3.5-turbo' })
101+
});
102+
const timeout_handle = setTimeout(() => {
103+
this.controller.abort();
104+
}, this.timeout);
105+
const ret = await response.json();
106+
clearTimeout(timeout_handle);
107+
return ret;
108+
}
109+
}
110+
111+
async embeddings(options) {
112+
// 拼接目标URL
113+
const url = this.apiBaseUrl + '/v1/embeddings';
114+
// 拼接headers
115+
const headers = {
116+
'Content-Type': 'application/json',
117+
Authorization: 'Bearer ' + this.key
118+
};
119+
const { model, ...restOptions } = options;
120+
// 使用 fetch 发送请求
121+
const response = await fetch(url, {
122+
signal: this.controller.signal,
123+
method: 'POST',
124+
headers: headers,
125+
body: JSON.stringify({ ...restOptions, model: model || 'text-embedding-ada-002' })
126+
});
127+
const timeout_handle = setTimeout(() => {
128+
this.controller.abort();
129+
}, this.timeout);
130+
const ret = await response.json();
131+
clearTimeout(timeout_handle);
132+
return ret;
133+
}
134+
135+
async billing() {
136+
const url = this.apiBaseUrl + '/dashboard/billing/credit_grants';
137+
const headers = {
138+
'Content-Type': 'application/json',
139+
Authorization: 'Bearer ' + this.key
140+
};
141+
const response = await fetch(url, {
142+
signal: this.controller.signal,
143+
method: 'GET',
144+
headers: headers
145+
});
146+
const timeout_handle = setTimeout(() => {
147+
this.controller.abort();
148+
}, this.timeout);
149+
const ret = await response.json();
150+
clearTimeout(timeout_handle);
151+
return ret;
152+
}
153+
};

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
"name": "api2d",
33
"version": "0.1.11",
44
"description": "pure browser sdk for api2d and openai",
5-
"main": "index.js",
5+
"main": "cjs/index.js",
6+
"module": "index.js",
7+
"types": "index.d.ts",
68
"repository": "https://github.com/easychen/api2d-js",
79
"author": "EasyChen",
810
"license": "MIT",
911
"private": false,
1012
"dependencies": {
11-
"@microsoft/fetch-event-source": "^2.0.1"
13+
"@microsoft/fetch-event-source": "^2.0.1",
14+
"eventsource-parser": "^1.0.0",
15+
"node-fetch": "^2.6.9"
1216
}
1317
}

0 commit comments

Comments
 (0)