@tsailab/xai
Version:
The loto-xai is an openai nodejs sdk compatible extension library.
484 lines • 17.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.XaiSseFetch = void 0;
const index_1 = require("./index");
const streamable_error_1 = require("./streamable.error");
const xai_error_codes_1 = require("../../error/xai-error.codes");
const streamable_parser_1 = require("./streamable.parser");
const xai_error_1 = require("../../error/xai-error");
const utils_1 = require("../../utils");
const defaultCloseHandler = (data) => {
if (data) {
globalThis.console.log(`Xai Request ${data.reqid} ${data.provider} completed.\n${data.result}`);
}
};
const defaultErrorHandler = (err) => {
globalThis.console.error('Xai streamable error', err);
};
const MAX_LISTENERS = 3;
const delayCloseMillionSeconds = 100;
const AUTO_ABORT_REASON = 'completed';
/**
*
*/
class XaiSseFetch {
checkAuth = true;
debug = false;
eventDataParsed = false;
method = 'POST';
controller;
apiPrefix = '/api/v3';
apiPath;
headers = {
'Content-type': 'application/json',
'accept': index_1.EventStreamContentType,
};
fetchInprogress = false;
reqCached = { reqid: '', msgid: '', created: 0 };
eventListeners = {};
sseCaches = {
messages: [],
result: '',
};
constructor(apiPath, options) {
if (!apiPath?.length)
throw new Error('Xai Streamable apiPath illegal.');
this.apiPath = apiPath;
if (!index_1.globalFetch)
throw new Error('your enviroment no fetch.');
this._registListeners(options);
}
get url() {
const url = this.apiPrefix?.endsWith('/')
? this.apiPrefix.substring(0, this.apiPrefix.length - 2)
: this.apiPrefix;
return this.apiPath?.startsWith('/')
? `${url}${this.apiPath}`
: `${url}/${this.apiPath}`;
}
/**
* request id
*/
get reqid() {
return this.reqCached.reqid;
}
get msgid() {
return this.reqCached.msgid;
}
get inprogress() {
return this.fetchInprogress;
}
get error() {
return this.reqCached?.error;
}
get result() {
return this.sseCaches.result;
}
log(data, type = 'warn', prefix) {
switch (type) {
case 'log':
this.debug &&
(prefix
? globalThis.console.log(prefix, data)
: globalThis.console.log(data));
break;
case 'warn':
this.debug &&
(prefix
? globalThis.console.warn(prefix, data)
: globalThis.console.warn(data));
break;
case 'error':
prefix
? globalThis.console.error(prefix, data)
: globalThis.console.error(data);
break;
default:
break;
}
}
/**
* @public
* launch fetch request
* @param data request data
* @param cb prepare request parametes callback
* @returns XaiStreamFetch instance
*/
async connect(data, cb) {
this.log(`Start debug connect >>>>>`, 'log');
// ignore inprogress
if (this.inprogress) {
this.log(`Preovus Requset ${this.reqid} had inprogress.`, 'error');
return this;
}
// initialize request inner cache
await this.createRequestCache(data);
await this.setQuestion(data);
// reset sse response caches
await this.resetPrepareCaches();
const { controller, headers, ...others } = data;
const reqHeaders = await this.prepareHeaders(headers);
// before luanch sse call
await cb?.(this.reqCached);
try {
this.controller = controller ?? new AbortController();
// remove first then add event
this.controller.signal.removeEventListener('abort', this.abortEventHandler.bind(this));
this.controller.signal.addEventListener('abort', this.abortEventHandler.bind(this));
const url = this.url;
this.log(`Request SSE ${url} with ${this.reqid}`, 'log');
const response = await (0, index_1.globalFetch)(url, {
method: this.method,
body: others ? JSON.stringify({ ...others, stream: true }) : undefined,
signal: this.controller.signal,
headers: {
...reqHeaders,
},
});
if (!response.ok) {
this.log(`Start debug connect contentType>>>>>${response.ok} ${response?.statusText}`, 'log');
throw streamable_error_1.SseFetchError.newClientError(response.status, response.statusText);
}
this.fetchInprogress = true;
const contentType = await response.headers.get('Content-Type');
if (!contentType?.includes(index_1.EventStreamContentType)) {
this.log(`Start debug connect contentType>>>>>${contentType}`, 'log');
throw streamable_error_1.SseFetchError.newClientError(xai_error_codes_1.SSE_ERROE_CODE, 'Please check remote api is SSE mode.');
}
const stream = response.body;
if (stream) {
const readChunkMessage = this._parseChunkMessage.bind(this);
await (0, streamable_parser_1.getBytes)(stream, (0, streamable_parser_1.getLines)(readChunkMessage()));
}
this.fetchInprogress = false;
return this;
}
catch (e) {
this.log(e, 'error');
this.updateSomeRequestCache({ error: e?.message });
if (e?.name === 'AbortError') {
this.log('You cancel the request.');
return this;
}
if (e instanceof streamable_error_1.SseFetchError || e instanceof xai_error_1.XaiError)
this.dispatchEvent('error', e);
else
this.dispatchEvent('error', streamable_error_1.SseFetchError.createFromError(e));
}
finally {
this.fetchInprogress = false;
}
return this;
}
/**
* disconnect
*/
disconnect() {
if (this.controller) {
const costTime = (0, utils_1.calcCostTime)(this.reqCached.created);
this.updateSomeSseCahces({ costTime });
this.controller.abort(AUTO_ABORT_REASON);
}
}
cancel() {
if (this.controller?.signal && this.controller.signal.aborted === false)
this.controller.abort(`Client user canceled this request ${this.reqid}`);
}
dispatchEvent(eventName, data) {
if (!this.eventListeners[eventName]?.length) {
this.log(data, 'log', `${eventName} not registed.`);
return;
}
this.eventListeners[eventName].forEach((handler) => {
handler(data);
});
}
/**
* when abort on completed will emit close
* @param _ev
*/
abortEventHandler(_ev) {
this.fetchInprogress = false;
if (this.controller?.signal.reason === AUTO_ABORT_REASON) {
const { reqid, msgid, created, chatid, provider, model } = this.reqCached;
const { result, costTime } = this.sseCaches;
const data = {
reqid,
msgid,
created,
chatid,
provider,
model,
costTime,
result,
};
this.dispatchEvent('close', data);
}
else if (this.controller?.signal.reason) {
this.log(this.controller.signal.reason, 'warn');
}
}
/**
* regist event
* @param eventName
* @param handler
*/
addListener(eventName, handler) {
if (!this.eventListeners[eventName])
this.eventListeners[eventName] = [];
if (this.eventListeners[eventName].length >= MAX_LISTENERS) {
this.log(`SSEFetch ${eventName} listeners more than ${MAX_LISTENERS} limit.`);
return this;
}
this.eventListeners[eventName].push(handler);
return this;
}
/**
*
* @param eventName
* @param handler
* @returns
*/
removeListener(eventName, handler) {
if (!this.eventListeners[eventName]?.length)
return this;
if (!handler) {
const size = this.eventListeners[eventName].length;
this.eventListeners[eventName].splice(0, size);
return this;
}
const idx = this.eventListeners[eventName].findIndex((h) => h === handler);
if (idx >= 0)
this.eventListeners[eventName].splice(idx, 1);
return this;
}
/**
* @private
* init request cache & put reqid into header
*
* @param data
* front request Data if reqid or msgid is null
* will auto created and fill to data
*/
async createRequestCache(data) {
const { uuid, chatid, model, provider } = data;
let { msgid, reqid } = data;
if (!msgid?.length) {
msgid = await (0, utils_1.createChatMessageId)(16, uuid);
data.msgid = msgid;
}
if (!reqid?.length) {
reqid = await (0, utils_1.createRequestId)();
data.reqid = reqid;
}
this.reqCached = {
created: Date.now(),
msgid,
reqid,
chatid,
model,
provider,
};
await this.setHeaderReqid(reqid);
}
/**
* before call sse reset repsonse cache
*/
resetPrepareCaches() {
this.sseCaches = {
messages: [],
result: '',
};
}
updateSomeSseCahces(some) {
this.sseCaches = {
...this.sseCaches,
...some,
};
}
/**
* merge headers and check Authorization token
* @param requestHeaders
* @returns headers
*/
prepareHeaders(requestHeaders) {
const Authorization = requestHeaders?.Authorization ?? this.headers?.Authorization;
if (this.checkAuth && !Authorization?.length) {
throw streamable_error_1.SseFetchError.newClientError(401, 'Required login Authorization token.');
}
return {
...this.headers,
...requestHeaders,
Authorization,
};
}
setHeaderReqid(reqid) {
this.headers['X-Loto-Reqid'] = reqid;
}
/**
*
* @param some
*/
updateSomeRequestCache(some) {
this.reqCached = {
...this.reqCached,
...some,
};
}
resetReqCache() {
this.reqCached = { reqid: '', msgid: '', created: 0 };
}
/**
*
* @param options
*/
_registListeners(options) {
const { checkAuth, eventDataParsed, apiBasePrefix, method, debug, bearerHeaders, token, onmessage, oncancel, onclose = defaultCloseHandler, onerror = defaultErrorHandler, } = options;
this.debug = Boolean(debug);
this.eventDataParsed = Boolean(eventDataParsed);
this.checkAuth = Boolean(checkAuth);
if (method?.length)
this.method = method;
if (apiBasePrefix?.length)
this.apiPrefix = apiBasePrefix;
this.headers = {
...this.headers,
...bearerHeaders,
};
if (token?.length) {
this.headers = {
...this.headers,
Authorization: `Bearer ${token}`,
};
}
if (typeof onmessage !== 'function') {
throw new TypeError('Parameter onmessage required an function of SseCallbackFn.');
}
this.addListener('message', onmessage);
this.addListener('close', onclose);
this.addListener('error', onerror);
if (oncancel)
this.addListener('cancel', oncancel);
}
pushMessageCache(message) {
if (!this.sseCaches.messages)
this.sseCaches.messages = [];
this.sseCaches.messages.push(message);
}
appendResult(chunk) {
const old = this.sseCaches.result ?? '';
this.updateSomeSseCahces({ result: `${old}${chunk}` });
}
setQuestion(data) {
const { text, messages } = data;
let q = text ?? '';
if (messages?.length && messages.slice(-1)[0].role === 'user') {
if (typeof messages.slice(-1)[0].content === 'string')
q = messages.slice(-1)[0].content;
}
if (q.length)
this.reqCached.question = q;
}
/**
* @private
*
* @returns function onLine(arr:Uint8Array,fieldLength:number)=>void
*/
_parseChunkMessage() {
let message = newMessage();
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
const decoder = new TextDecoder();
return function onLine(line, fieldLength) {
if (line.length === 0) {
// message has filled by uint8array
if (message.event === 'error' && message.data?.length) {
that.log(message, 'error', `XSse ${that.url} ${that.reqid}`);
throw streamable_error_1.SseFetchError.fromSseErrorData(message.data);
}
// empty line denotes end of message.
// Trigger the callback and start a new message:
if (message.data.length) {
that.pushMessageCache(message);
let chunkData;
if (typeof message.data === 'object') {
// nestjs not support sse data object
that.log(message.data, 'log', 'SSE data is object');
chunkData = message.data;
}
else {
try {
chunkData = JSON.parse(message.data);
}
catch (e) {
that.log(e, 'error', 'parse message.data error');
}
}
if (chunkData?.content)
that.appendResult(chunkData.content);
// message
if (chunkData) {
that.eventDataParsed
? that.dispatchEvent('message', chunkData)
: that.dispatchEvent('message', message);
}
// Fix qianfan reason is normal but deepseek or gpt is stop
// is_end is xai proxy translate to boolean
if (chunkData?.finish_reason || chunkData?.is_end) {
setTimeout(() => {
that.disconnect();
}, delayCloseMillionSeconds);
}
}
// an new message
message = newMessage();
}
else if (fieldLength > 0) {
// exclude comments and lines with no values
// line is of format "<field>:<value>" or "<field>: <value>"
// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
const field = decoder.decode(line.subarray(0, fieldLength));
const valueOffset = fieldLength +
(line[fieldLength + 1] === streamable_parser_1.SSEContolChars.Space ? 2 : 1);
const value = decoder.decode(line.subarray(valueOffset));
that.log(`Recived ${field} ${value}`, 'log', 'Debug Xai SSE : ');
switch (field) {
case 'data':
// if this message already has data, append the new value to the old.
// otherwise, just set to the new value:
message.data = message.data ? `${message.data}\n${value}` : value;
break;
case 'event':
that.log(`Message event type is ${value}`, 'log');
message.event = value;
break;
case 'id':
that.updateSomeSseCahces({ prevousId: (message.id = value) });
break;
case 'retry':
// eslint-disable-next-line no-case-declarations
const retry = Number.parseInt(value, 10);
if (!Number.isNaN(retry)) {
// per spec, ignore non-integers
message.retry = retry;
}
break;
default:
that.log(`Xai SSE unhandle field ${field}`, 'warn');
break;
}
}
// else if end
};
}
}
exports.XaiSseFetch = XaiSseFetch;
function newMessage() {
return {
id: '',
event: '',
data: '',
retry: undefined,
};
}
//# sourceMappingURL=xai.sse.fetch.js.map