UNPKG

@tsailab/xai

Version:

The loto-xai is an openai nodejs sdk compatible extension library.

484 lines 17.3 kB
"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