@xsai/shared 0.5.1 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # @xsai/shared
2
+
3
+ <!-- automd:file src="/docs/snippets/shared.md" -->
4
+
5
+ ```sh
6
+ pnpm add @xsai/shared
7
+ ```
8
+
9
+ ```ts
10
+ import { HttpError, sendRequest } from '@xsai/shared'
11
+
12
+ try {
13
+ await sendRequest({ method: 'GET', path: 'models' }, {
14
+ baseURL: 'https://example.com/v1/',
15
+ fetch: async () => new Response('Try again later', { status: 429 }),
16
+ })
17
+ }
18
+ catch (error) {
19
+ if (HttpError.isInstance(error))
20
+ console.log(error.status, error.body)
21
+ else
22
+ throw error
23
+ }
24
+ ```
25
+
26
+ <!-- /automd -->
27
+
28
+ Read the [documentation](https://xsai.js.org/shared).
package/dist/index.d.ts CHANGED
@@ -1,144 +1,157 @@
1
- interface APICallErrorOptions extends ErrorOptions {
2
- requestBody?: string;
3
- response: Response;
4
- responseBody?: string;
5
- }
6
- interface InvalidResponseErrorOptions extends ErrorOptions {
7
- body?: unknown;
8
- contentType?: null | string;
9
- reason: InvalidResponseReason;
10
- response?: Response;
11
- responseBody?: string;
12
- }
13
- type InvalidResponseReason = 'empty_body' | 'invalid_body' | 'no_choices';
14
- interface InvalidToolCallErrorOptions extends ErrorOptions {
15
- availableTools?: string[];
16
- reason: InvalidToolCallReason;
17
- toolCall: unknown;
18
- toolName?: string;
19
- }
20
- type InvalidToolCallReason = 'missing_arguments' | 'missing_name' | 'tool_call_id_mismatch' | 'unknown_tool';
21
- interface InvalidToolInputErrorOptions extends ErrorOptions {
22
- toolInput: unknown;
23
- toolName: string;
24
- }
25
- interface JSONParseErrorOptions extends ErrorOptions {
26
- text: string;
27
- }
28
- interface RemoteAPIErrorOptions extends ErrorOptions {
29
- response?: Response;
30
- responseBody: string;
31
- }
32
- interface ToolExecutionErrorOptions extends InvalidToolInputErrorOptions {
33
- toolCallId?: string;
34
- }
35
- type XSAIErrorCode = 'api_call_error' | 'invalid_response' | 'invalid_tool_call' | 'invalid_tool_input' | 'json_parse_error' | 'remote_api_error' | 'tool_execution_error';
36
- declare class XSAIError extends Error {
37
- code: (string & {}) | XSAIErrorCode;
38
- constructor(message: string, code: (string & {}) | XSAIErrorCode, options?: ErrorOptions);
39
- static isInstance<T extends abstract new (...args: any[]) => XSAIError>(this: T, error: unknown): error is InstanceType<T>;
40
- }
41
- declare class APICallError extends XSAIError {
42
- requestBody?: string;
43
- response: Response;
44
- responseBody?: string;
45
- responseHeaders: Record<string, string>;
46
- statusCode: number;
47
- statusText: string;
48
- url: string;
49
- constructor(message: string, options: APICallErrorOptions);
50
- }
51
- declare class InvalidResponseError extends XSAIError {
52
- body?: unknown;
53
- contentType?: null | string;
54
- reason: InvalidResponseErrorOptions['reason'];
55
- response?: Response;
56
- responseBody?: string;
57
- constructor(message: string, options: InvalidResponseErrorOptions);
58
- }
59
- declare class InvalidToolCallError extends XSAIError {
60
- availableTools?: string[];
61
- reason: InvalidToolCallErrorOptions['reason'];
62
- toolCall: unknown;
63
- toolName?: string;
64
- constructor(message: string, options: InvalidToolCallErrorOptions);
65
- }
66
- declare class InvalidToolInputError extends XSAIError {
67
- toolInput: unknown;
68
- toolName: string;
69
- constructor(message: string, options: InvalidToolInputErrorOptions);
70
- }
71
- declare class JSONParseError extends XSAIError {
72
- text: string;
73
- constructor(message: string, options: JSONParseErrorOptions);
74
- }
75
- declare class RemoteAPIError extends XSAIError {
76
- response?: Response;
77
- responseBody: string;
78
- constructor(message: string, options: RemoteAPIErrorOptions);
79
- }
80
- declare class ToolExecutionError extends XSAIError {
81
- toolCallId?: string;
82
- toolInput: unknown;
83
- toolName: string;
84
- constructor(message: string, options: ToolExecutionErrorOptions);
85
- }
86
-
87
- type Fetch = (input: URL, init: RequestInit) => Promise<Response>;
88
-
89
- interface CommonRequestOptions {
90
- abortSignal?: AbortSignal;
1
+ interface HttpOptions {
91
2
  apiKey?: string;
92
3
  /** @example `https://api.openai.com/v1/` */
93
4
  baseURL: string | URL;
94
- fetch?: Fetch | typeof globalThis.fetch;
95
- /** @default `undefined` */
96
- headers?: Headers | Record<string, string>;
97
- /** @example `gpt-4o` */
5
+ fetch?: (request: Request) => Promise<Response>;
6
+ headers?: Record<string, string | undefined>;
98
7
  model: string;
99
8
  }
100
9
 
101
- type WithUnknown<T> = T & {
102
- [key: string]: unknown;
103
- };
104
-
105
- declare const strCamelToSnake: (str: string) => string;
106
- declare const objCamelToSnake: (obj: Record<string, unknown>) => {
107
- [k: string]: unknown;
108
- };
10
+ type Promisable<T> = Promise<T> | T;
109
11
 
12
+ type XSAIErrorCause<T extends XSAIErrorCode> = Exclude<XSAIErrorCauseMap[T], undefined>;
110
13
  /**
111
- * Clean all undefined values.
112
- * @param obj object
113
- * @returns cleaned object
114
- *
115
- * @example
116
- * ```ts
117
- * import { clean } from '@xsai/shared'
118
- *
119
- * // { a: 'a' }
120
- * const obj = clean({
121
- * a: 'a',
122
- * b: undefined
123
- * })
124
- * ```
14
+ * Per-code `cause` contract: `undefined` forbids a cause, `unknown` makes it
15
+ * optional, `NonNullable<unknown>` requires one. Packages augment this
16
+ * interface to register their own codes.
125
17
  */
126
- declare const clean: <T extends Record<string, undefined | unknown>>(obj: T) => Record<keyof T, Exclude<T[keyof T], unknown>>;
127
-
128
- declare const postJSON: (path: string, options: WithUnknown<CommonRequestOptions>) => Promise<Response>;
129
-
130
- declare const requestBody: (body: Record<string, unknown>) => string;
131
-
132
- declare const requestHeaders: (headers?: CommonRequestOptions["headers"], apiKey?: CommonRequestOptions["apiKey"]) => Record<"Authorization", never>;
18
+ interface XSAIErrorCauseMap {
19
+ 'http-error': undefined;
20
+ 'invalid-input': undefined;
21
+ 'invalid-response': unknown;
22
+ 'network-error': NonNullable<unknown>;
23
+ 'truncated-stream': undefined;
24
+ }
25
+ type XSAIErrorCode = keyof XSAIErrorCauseMap;
26
+ type XSAIErrorOptions<T extends XSAIErrorCode> = undefined extends XSAIErrorCauseMap[T] ? [options?: {
27
+ cause?: XSAIErrorCause<T>;
28
+ }] : [options: {
29
+ cause: XSAIErrorCause<T>;
30
+ }];
31
+ declare class XSAIError<T extends XSAIErrorCode = XSAIErrorCode> extends Error {
32
+ readonly code: T;
33
+ constructor(code: T, message: string, ...[options]: XSAIErrorOptions<T>);
34
+ static isInstance<T extends XSAIError>(this: new (...args: never[]) => T, error: unknown): error is T;
35
+ }
36
+ declare class HttpError extends XSAIError<'http-error'> {
37
+ readonly body: string;
38
+ /** The raw response headers, for debugging (e.g. `x-request-id`, `retry-after`). */
39
+ readonly headers: Headers;
40
+ readonly status: number;
41
+ constructor({ body, headers, status }: {
42
+ body: string;
43
+ headers: Headers;
44
+ status: number;
45
+ }, ...options: XSAIErrorOptions<'http-error'>);
46
+ }
133
47
 
48
+ interface SendRequestOptions {
49
+ body?: FormData | Record<string, unknown>;
50
+ headers?: Record<string, string | undefined>;
51
+ method?: 'GET' | 'POST';
52
+ path: string;
53
+ signal?: AbortSignal;
54
+ }
134
55
  declare const requestURL: (path: string, baseURL: string | URL) => URL;
56
+ declare const sendRequest: (options: SendRequestOptions, httpOptions: Omit<HttpOptions, "model">) => Promise<Response & {
57
+ body: NonNullable<Response["body"]>;
58
+ }>;
135
59
 
136
- declare const responseCatch: (res: Response) => Promise<Response>;
137
-
138
- declare const responseJSON: <T>(res: Response) => Promise<T>;
60
+ /**
61
+ * A parsed EventSource message event
62
+ *
63
+ * @public
64
+ */
65
+ interface EventSourceMessage {
66
+ /**
67
+ * The event type sent from the server. Note that this differs from the browser `EventSource`
68
+ * implementation in that browsers will default this to `message`, whereas this parser will
69
+ * leave this as `undefined` if not explicitly declared.
70
+ */
71
+ event?: string | undefined;
72
+ /**
73
+ * ID of the message, if any was provided by the server. Can be used by clients to keep the
74
+ * last received message ID in sync when reconnecting.
75
+ */
76
+ id?: string | undefined;
77
+ /**
78
+ * The data received for this message
79
+ */
80
+ data: string;
81
+ }
139
82
 
140
- type TrampolineFn<T> = (() => Promise<TrampolineFn<T>> | TrampolineFn<T>) | Promise<T> | T;
141
- declare const trampoline: <T>(fn: () => Promise<TrampolineFn<T>> | TrampolineFn<T>) => Promise<T>;
83
+ /**
84
+ * Options for the EventSourceParserStream.
85
+ *
86
+ * @public
87
+ */
88
+ interface StreamOptions {
89
+ /**
90
+ * Callback for an `id` field when a blank line ends an event block.
91
+ *
92
+ * Called once with the last valid `id` value in the block, whether or not the block contains a
93
+ * `data` field. When the block produces an event, `onId` is called before the event is enqueued.
94
+ * An empty `id` field is reported as an empty string. An `id` field containing U+0000 is ignored.
95
+ *
96
+ * @param id - The value of the last valid `id` field in the block
97
+ */
98
+ onId?: ((id: string) => void) | undefined;
99
+ /**
100
+ * Behavior when a parsing error occurs.
101
+ *
102
+ * - A custom function can be provided to handle the error.
103
+ * - `'terminate'` will error the stream and stop parsing.
104
+ * - Any other value will ignore the error and continue parsing.
105
+ *
106
+ * @defaultValue `undefined`
107
+ */
108
+ onError?: ('terminate' | ((error: Error) => void)) | undefined;
109
+ /**
110
+ * Callback for when a reconnection interval is sent from the server.
111
+ *
112
+ * @param retry - The number of milliseconds to wait before reconnecting.
113
+ */
114
+ onRetry?: ((retry: number) => void) | undefined;
115
+ /**
116
+ * Callback for when a comment is encountered in the stream.
117
+ *
118
+ * @param comment - The comment encountered in the stream.
119
+ */
120
+ onComment?: ((comment: string) => void) | undefined;
121
+ /**
122
+ * Maximum number of characters the parser is allowed to buffer across calls to `feed()`.
123
+ *
124
+ * When the limit is exceeded, the stream is always errored (regardless of the `onError`
125
+ * setting) since the underlying parser is unrecoverable without a `reset()`.
126
+ *
127
+ * @defaultValue `undefined` (unbounded)
128
+ */
129
+ maxBufferSize?: number | undefined;
130
+ }
131
+ /**
132
+ * A TransformStream that ingests a stream of strings and produces a stream of `EventSourceMessage`.
133
+ *
134
+ * @example Basic usage
135
+ * ```
136
+ * const eventStream =
137
+ * response.body
138
+ * .pipeThrough(new TextDecoderStream())
139
+ * .pipeThrough(new EventSourceParserStream())
140
+ * ```
141
+ *
142
+ * @example Terminate stream on parsing errors
143
+ * ```
144
+ * const eventStream =
145
+ * response.body
146
+ * .pipeThrough(new TextDecoderStream())
147
+ * .pipeThrough(new EventSourceParserStream({onError: 'terminate'}))
148
+ * ```
149
+ *
150
+ * @public
151
+ */
152
+ declare class EventSourceParserStream extends TransformStream<string, EventSourceMessage> {
153
+ constructor({ onError, onRetry, onComment, onId, maxBufferSize }?: StreamOptions);
154
+ }
142
155
 
143
- export { APICallError, InvalidResponseError, InvalidToolCallError, InvalidToolInputError, JSONParseError, RemoteAPIError, ToolExecutionError, XSAIError, clean, objCamelToSnake, postJSON, requestBody, requestHeaders, requestURL, responseCatch, responseJSON, strCamelToSnake, trampoline };
144
- export type { APICallErrorOptions, CommonRequestOptions, Fetch, InvalidResponseErrorOptions, InvalidResponseReason, InvalidToolCallErrorOptions, InvalidToolCallReason, InvalidToolInputErrorOptions, JSONParseErrorOptions, RemoteAPIErrorOptions, ToolExecutionErrorOptions, TrampolineFn, WithUnknown, XSAIErrorCode };
156
+ export { EventSourceParserStream, HttpError, XSAIError, requestURL, sendRequest };
157
+ export type { EventSourceMessage, HttpOptions, Promisable, SendRequestOptions, XSAIErrorCause, XSAIErrorCauseMap, XSAIErrorCode, XSAIErrorOptions };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  class XSAIError extends Error {
2
- constructor(message, code, options = {}) {
3
- super(message, { cause: options.cause });
2
+ code;
3
+ constructor(code, message, ...[options]) {
4
+ super(message, options);
4
5
  this.code = code;
5
6
  this.name = new.target.name;
6
7
  }
@@ -8,134 +9,418 @@ class XSAIError extends Error {
8
9
  return error instanceof this;
9
10
  }
10
11
  }
11
- class APICallError extends XSAIError {
12
- constructor(message, options) {
13
- super(message, "api_call_error", options);
14
- this.requestBody = options.requestBody;
15
- this.response = options.response;
16
- this.responseBody = options.responseBody;
17
- this.responseHeaders = Object.fromEntries(options.response.headers.entries());
18
- this.statusCode = options.response.status;
19
- this.statusText = options.response.statusText;
20
- this.url = options.response.url;
12
+ class HttpError extends XSAIError {
13
+ body;
14
+ /** The raw response headers, for debugging (e.g. `x-request-id`, `retry-after`). */
15
+ headers;
16
+ status;
17
+ constructor({ body, headers, status }, ...options) {
18
+ super("http-error", `HTTP ${status}: ${body}`, ...options);
19
+ this.body = body;
20
+ this.headers = headers;
21
+ this.status = status;
21
22
  }
22
23
  }
23
- class InvalidResponseError extends XSAIError {
24
+
25
+ const requestURL = (path, baseURL) => {
26
+ const base = baseURL.toString();
27
+ return new URL(path, base.endsWith("/") ? base : `${base}/`);
28
+ };
29
+ const sendRequest = async (options, httpOptions) => {
30
+ const url = requestURL(options.path, httpOptions.baseURL);
31
+ const headers = options.headers ?? {
32
+ ...httpOptions.headers,
33
+ "Authorization": httpOptions.apiKey == null ? httpOptions.headers?.Authorization : `Bearer ${httpOptions.apiKey}`,
34
+ "Content-Type": options.body instanceof FormData ? void 0 : options.body == null ? httpOptions.headers?.["Content-Type"] : "application/json"
35
+ };
36
+ const responseCatch = async (res) => {
37
+ if (!res.ok)
38
+ throw new HttpError({ body: await res.text(), headers: res.headers, status: res.status });
39
+ if (res.body == null)
40
+ throw new XSAIError("invalid-response", "Response body is empty");
41
+ return res;
42
+ };
43
+ const requestCatch = (cause) => {
44
+ options.signal?.throwIfAborted();
45
+ throw new XSAIError("network-error", `request to ${url.toString()} failed`, {
46
+ cause: cause ?? new Error("fetch rejected without a reason")
47
+ });
48
+ };
49
+ const request = new Request(url, {
50
+ body: options.body == null ? void 0 : options.body instanceof FormData ? options.body : JSON.stringify(options.body),
51
+ headers: Object.fromEntries(Object.entries(headers).filter(
52
+ ([name, value]) => value !== void 0 && (!(options.body instanceof FormData) || name.toLowerCase() !== "content-type")
53
+ )),
54
+ method: options.method ?? "POST",
55
+ signal: options.signal
56
+ });
57
+ return (httpOptions.fetch ?? fetch)(request).then(responseCatch, requestCatch);
58
+ };
59
+
60
+ class ParseError extends Error {
24
61
  constructor(message, options) {
25
- super(message, "invalid_response", options);
26
- Object.assign(this, options);
62
+ super(message);
63
+ this.name = "ParseError";
64
+ this.type = options.type;
65
+ this.field = options.field;
66
+ this.value = options.value;
67
+ this.line = options.line;
27
68
  }
28
69
  }
29
- class InvalidToolCallError extends XSAIError {
30
- constructor(message, options) {
31
- super(message, "invalid_tool_call", options);
32
- Object.assign(this, options);
70
+
71
+ const LF = 10;
72
+ const CR = 13;
73
+ const SPACE = 32;
74
+ const MAX_FIELD_PREFIX_LENGTH = 6;
75
+ function createParser(config) {
76
+ if (typeof config === "function") {
77
+ throw new TypeError("`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?");
33
78
  }
34
- }
35
- class InvalidToolInputError extends XSAIError {
36
- constructor(message, options) {
37
- super(message, "invalid_tool_input", options);
38
- Object.assign(this, options);
79
+ const { maxBufferSize, onComment, onError, onEvent, onId, onRetry } = config;
80
+ const pendingFragments = [];
81
+ let pendingFragmentsLength = 0;
82
+ let bomPrefix = "";
83
+ let id;
84
+ let data = "";
85
+ let dataLines = 0;
86
+ let eventType;
87
+ let terminated = false;
88
+ let skippingLine = false;
89
+ let skipNextLineFeed = false;
90
+ function feed(chunk) {
91
+ if (terminated) {
92
+ throw new Error("Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing.");
93
+ }
94
+ if (bomPrefix !== void 0) {
95
+ chunk = bomPrefix + chunk;
96
+ if (chunk === "" || chunk === "\xEF" || chunk === "\xEF\xBB") {
97
+ bomPrefix = chunk;
98
+ return;
99
+ }
100
+ bomPrefix = void 0;
101
+ chunk = chunk.replace(/^(?:\uFEFF|\xEF\xBB\xBF)/, "");
102
+ }
103
+ if (skippingLine || skipNextLineFeed) {
104
+ chunk = resumeAfterSkip(chunk);
105
+ if (!chunk) {
106
+ return;
107
+ }
108
+ }
109
+ if (!pendingFragments.length) {
110
+ const trailing = processLines(chunk);
111
+ if (trailing !== "") {
112
+ storeTrailing(trailing);
113
+ }
114
+ checkBufferSize();
115
+ return;
116
+ }
117
+ if (chunk.indexOf("\n") === -1 && chunk.indexOf("\r") === -1) {
118
+ if (pendingFragmentsLength < MAX_FIELD_PREFIX_LENGTH) {
119
+ const head = pendingFragments.join("") + chunk.slice(0, MAX_FIELD_PREFIX_LENGTH - pendingFragmentsLength);
120
+ if (!shouldBufferTrailing(head)) {
121
+ pendingFragments.length = 0;
122
+ pendingFragmentsLength = 0;
123
+ skippingLine = true;
124
+ return;
125
+ }
126
+ }
127
+ pendingFragments.push(chunk);
128
+ pendingFragmentsLength += chunk.length;
129
+ checkBufferSize();
130
+ return;
131
+ }
132
+ pendingFragments.push(chunk);
133
+ const input = pendingFragments.join("");
134
+ pendingFragments.length = 0;
135
+ pendingFragmentsLength = 0;
136
+ storeTrailing(processLines(input));
137
+ checkBufferSize();
39
138
  }
40
- }
41
- class JSONParseError extends XSAIError {
42
- constructor(message, options) {
43
- super(message, "json_parse_error", options);
44
- this.text = options.text;
139
+ function resumeAfterSkip(chunk) {
140
+ if (chunk.length === 0) {
141
+ return chunk;
142
+ }
143
+ if (skipNextLineFeed) {
144
+ skipNextLineFeed = false;
145
+ return chunk.charCodeAt(0) === LF ? chunk.slice(1) : chunk;
146
+ }
147
+ const crIndex = chunk.indexOf("\r");
148
+ const lfIndex = chunk.indexOf("\n");
149
+ const lineEnd = crIndex === -1 ? lfIndex : lfIndex === -1 ? crIndex : crIndex < lfIndex ? crIndex : lfIndex;
150
+ if (lineEnd === -1) {
151
+ return "";
152
+ }
153
+ if (lineEnd === chunk.length - 1 && chunk.charCodeAt(lineEnd) === CR) {
154
+ skippingLine = false;
155
+ skipNextLineFeed = true;
156
+ return "";
157
+ }
158
+ skippingLine = false;
159
+ return chunk.slice(lineEnd + (chunk.charCodeAt(lineEnd) === CR && chunk.charCodeAt(lineEnd + 1) === LF ? 2 : 1));
45
160
  }
46
- }
47
- class RemoteAPIError extends XSAIError {
48
- constructor(message, options) {
49
- super(message, "remote_api_error", options);
50
- Object.assign(this, options);
161
+ function storeTrailing(trailing) {
162
+ if (!trailing)
163
+ return;
164
+ if (trailing.charCodeAt(trailing.length - 1) === CR) {
165
+ parseLine(trailing, 0, trailing.length - 1);
166
+ skipNextLineFeed = true;
167
+ return;
168
+ }
169
+ if (shouldBufferTrailing(trailing)) {
170
+ pendingFragments.push(trailing);
171
+ pendingFragmentsLength = trailing.length;
172
+ return;
173
+ }
174
+ skippingLine = true;
51
175
  }
52
- }
53
- class ToolExecutionError extends XSAIError {
54
- constructor(message, options) {
55
- super(message, "tool_execution_error", options);
56
- Object.assign(this, options);
176
+ function shouldBufferTrailing(trailing) {
177
+ const firstCharCode = trailing.charCodeAt(0);
178
+ return firstCharCode === 58 && !!onComment || firstCharCode === 100 && isPotentialField(trailing, "data") || firstCharCode === 101 && isPotentialField(trailing, "event") || firstCharCode === 105 && isPotentialField(trailing, "id") || firstCharCode === 114 && isPotentialField(trailing, "retry");
57
179
  }
58
- }
59
-
60
- const strCamelToSnake = (str) => str.replace(/[A-Z]/g, (s) => `_${s.toLowerCase()}`);
61
- const objCamelToSnake = (obj) => Object.fromEntries(Object.entries(obj).map(([k, v]) => [strCamelToSnake(k), v]));
62
-
63
- const clean = (obj) => Object.fromEntries(
64
- Object.entries(obj).filter(([, v]) => v !== void 0)
65
- );
66
-
67
- const requestBody = (body) => JSON.stringify(objCamelToSnake(clean({
68
- ...body,
69
- abortSignal: void 0,
70
- apiKey: void 0,
71
- baseURL: void 0,
72
- fetch: void 0,
73
- headers: void 0
74
- })));
75
-
76
- const requestHeaders = (headers, apiKey) => clean({
77
- Authorization: apiKey !== void 0 ? `Bearer ${apiKey}` : void 0,
78
- ...headers
79
- });
80
-
81
- const requestURL = (path, baseURL) => {
82
- const base = baseURL.toString();
83
- return new URL(path, base.endsWith("/") ? base : `${base}/`);
84
- };
85
-
86
- const responseCatch = async (res) => {
87
- if (!res.ok) {
88
- const responseBody = await res.text();
89
- throw new APICallError(`Remote sent ${res.status} response: ${responseBody}`, {
90
- response: res,
91
- responseBody
92
- });
180
+ function checkBufferSize() {
181
+ if (maxBufferSize === void 0)
182
+ return;
183
+ if (pendingFragmentsLength + data.length <= maxBufferSize)
184
+ return;
185
+ terminated = true;
186
+ pendingFragments.length = 0;
187
+ pendingFragmentsLength = 0;
188
+ id = void 0;
189
+ data = "";
190
+ dataLines = 0;
191
+ eventType = void 0;
192
+ skippingLine = false;
193
+ skipNextLineFeed = false;
194
+ onError === null || onError === void 0 ? void 0 : onError(new ParseError(`Buffered data exceeded max buffer size of ${maxBufferSize} characters`, {
195
+ type: "max-buffer-size-exceeded"
196
+ }));
93
197
  }
94
- if (!res.body) {
95
- throw new InvalidResponseError("Response body is empty from remote server", {
96
- reason: "empty_body",
97
- response: res
98
- });
198
+ function processLines(chunk) {
199
+ let searchIndex = 0;
200
+ if (chunk.indexOf("\r") === -1) {
201
+ let lfIndex = chunk.indexOf("\n", searchIndex);
202
+ while (lfIndex !== -1) {
203
+ if (searchIndex === lfIndex) {
204
+ if (id !== void 0) {
205
+ onId === null || onId === void 0 ? void 0 : onId(id);
206
+ }
207
+ if (dataLines > 0) {
208
+ onEvent === null || onEvent === void 0 ? void 0 : onEvent({ id, event: eventType, data });
209
+ }
210
+ id = void 0;
211
+ data = "";
212
+ dataLines = 0;
213
+ eventType = void 0;
214
+ searchIndex = lfIndex + 1;
215
+ lfIndex = chunk.indexOf("\n", searchIndex);
216
+ continue;
217
+ }
218
+ const firstCharCode = chunk.charCodeAt(searchIndex);
219
+ if (isDataPrefix(chunk, searchIndex, firstCharCode)) {
220
+ const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5;
221
+ const value = chunk.slice(valueStart, lfIndex);
222
+ if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {
223
+ if (id !== void 0) {
224
+ onId === null || onId === void 0 ? void 0 : onId(id);
225
+ }
226
+ onEvent === null || onEvent === void 0 ? void 0 : onEvent({ id, event: eventType, data: value });
227
+ id = void 0;
228
+ data = "";
229
+ eventType = void 0;
230
+ searchIndex = lfIndex + 2;
231
+ lfIndex = chunk.indexOf("\n", searchIndex);
232
+ continue;
233
+ }
234
+ data = dataLines === 0 ? value : `${data}
235
+ ${value}`;
236
+ dataLines++;
237
+ } else if (isEventPrefix(chunk, searchIndex, firstCharCode)) {
238
+ eventType = chunk.slice(chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6, lfIndex) || void 0;
239
+ } else {
240
+ parseLine(chunk, searchIndex, lfIndex);
241
+ }
242
+ searchIndex = lfIndex + 1;
243
+ lfIndex = chunk.indexOf("\n", searchIndex);
244
+ }
245
+ return chunk.slice(searchIndex);
246
+ }
247
+ while (searchIndex < chunk.length) {
248
+ const crIndex = chunk.indexOf("\r", searchIndex);
249
+ const lfIndex = chunk.indexOf("\n", searchIndex);
250
+ let lineEnd = -1;
251
+ if (crIndex !== -1 && lfIndex !== -1) {
252
+ lineEnd = crIndex < lfIndex ? crIndex : lfIndex;
253
+ } else if (crIndex !== -1) {
254
+ if (crIndex === chunk.length - 1) {
255
+ lineEnd = -1;
256
+ } else {
257
+ lineEnd = crIndex;
258
+ }
259
+ } else if (lfIndex !== -1) {
260
+ lineEnd = lfIndex;
261
+ }
262
+ if (lineEnd === -1) {
263
+ break;
264
+ }
265
+ parseLine(chunk, searchIndex, lineEnd);
266
+ searchIndex = lineEnd + 1;
267
+ if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) {
268
+ searchIndex++;
269
+ }
270
+ }
271
+ return chunk.slice(searchIndex);
99
272
  }
100
- if (!(res.body instanceof ReadableStream)) {
101
- const contentType = res.headers.get("Content-Type");
102
- throw new InvalidResponseError(`Expected Response body to be a ReadableStream, but got ${String(res.body)}; Content Type is ${contentType}`, {
103
- body: res.body,
104
- contentType,
105
- reason: "invalid_body",
106
- response: res
107
- });
273
+ function parseLine(chunk, start, end) {
274
+ if (start === end) {
275
+ dispatchEvent();
276
+ return;
277
+ }
278
+ const firstCharCode = chunk.charCodeAt(start);
279
+ if (isDataPrefix(chunk, start, firstCharCode)) {
280
+ const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5;
281
+ const value2 = chunk.slice(valueStart, end);
282
+ data = dataLines === 0 ? value2 : `${data}
283
+ ${value2}`;
284
+ dataLines++;
285
+ return;
286
+ }
287
+ if (isEventPrefix(chunk, start, firstCharCode)) {
288
+ eventType = chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || void 0;
289
+ return;
290
+ }
291
+ if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) {
292
+ const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end);
293
+ if (!value2.includes("\0"))
294
+ id = value2;
295
+ return;
296
+ }
297
+ if (firstCharCode === 58) {
298
+ if (onComment) {
299
+ const line2 = chunk.slice(start, end);
300
+ onComment(line2.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1));
301
+ }
302
+ return;
303
+ }
304
+ const line = chunk.slice(start, end);
305
+ const fieldSeparatorIndex = line.indexOf(":");
306
+ if (fieldSeparatorIndex === -1) {
307
+ processField(line, "", line);
308
+ return;
309
+ }
310
+ const field = line.slice(0, fieldSeparatorIndex);
311
+ const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1;
312
+ const value = line.slice(fieldSeparatorIndex + offset);
313
+ processField(field, value, line);
108
314
  }
109
- return res;
110
- };
111
-
112
- const postJSON = async (path, options) => (options.fetch ?? globalThis.fetch)(requestURL(path, options.baseURL), {
113
- body: requestBody(options),
114
- headers: requestHeaders({
115
- "Content-Type": "application/json",
116
- ...options.headers
117
- }, options.apiKey),
118
- method: "POST",
119
- signal: options.abortSignal
120
- }).then(responseCatch);
315
+ function processField(field, value, line) {
316
+ switch (field) {
317
+ case "event":
318
+ eventType = value || void 0;
319
+ break;
320
+ case "data":
321
+ data = dataLines === 0 ? value : `${data}
322
+ ${value}`;
323
+ dataLines++;
324
+ break;
325
+ case "id":
326
+ if (!value.includes("\0"))
327
+ id = value;
328
+ break;
329
+ case "retry":
330
+ if (/^\d+$/.test(value)) {
331
+ onRetry === null || onRetry === void 0 ? void 0 : onRetry(parseInt(value, 10));
332
+ } else {
333
+ onError === null || onError === void 0 ? void 0 : onError(new ParseError(`Invalid \`retry\` value: "${value}"`, {
334
+ type: "invalid-retry",
335
+ value,
336
+ line
337
+ }));
338
+ }
339
+ break;
340
+ default:
341
+ onError === null || onError === void 0 ? void 0 : onError(new ParseError(`Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`, { type: "unknown-field", field, value, line }));
342
+ break;
343
+ }
344
+ }
345
+ function dispatchEvent() {
346
+ if (id !== void 0) {
347
+ onId === null || onId === void 0 ? void 0 : onId(id);
348
+ }
349
+ if (dataLines > 0) {
350
+ onEvent === null || onEvent === void 0 ? void 0 : onEvent({
351
+ id,
352
+ event: eventType,
353
+ data
354
+ });
355
+ }
356
+ id = void 0;
357
+ data = "";
358
+ dataLines = 0;
359
+ eventType = void 0;
360
+ }
361
+ function reset(options = {}) {
362
+ if (options.consume && pendingFragments.length > 0) {
363
+ const incompleteLine = pendingFragments.join("");
364
+ parseLine(incompleteLine, 0, incompleteLine.length);
365
+ }
366
+ bomPrefix = "";
367
+ id = void 0;
368
+ data = "";
369
+ dataLines = 0;
370
+ eventType = void 0;
371
+ pendingFragments.length = 0;
372
+ pendingFragmentsLength = 0;
373
+ terminated = false;
374
+ skippingLine = false;
375
+ skipNextLineFeed = false;
376
+ }
377
+ return { feed, reset };
378
+ }
379
+ function isDataPrefix(chunk, i, firstCharCode) {
380
+ return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58;
381
+ }
382
+ function isEventPrefix(chunk, i, firstCharCode) {
383
+ return firstCharCode === 101 && chunk.charCodeAt(i + 1) === 118 && chunk.charCodeAt(i + 2) === 101 && chunk.charCodeAt(i + 3) === 110 && chunk.charCodeAt(i + 4) === 116 && chunk.charCodeAt(i + 5) === 58;
384
+ }
385
+ function isPotentialField(line, field) {
386
+ let i = 1;
387
+ while (i < line.length && i < field.length) {
388
+ if (line.charCodeAt(i) !== field.charCodeAt(i)) {
389
+ return false;
390
+ }
391
+ i++;
392
+ }
393
+ return line.length <= field.length || line.charCodeAt(field.length) === 58;
394
+ }
121
395
 
122
- const responseJSON = async (res) => {
123
- const text = await res.text();
124
- try {
125
- return JSON.parse(text);
126
- } catch (cause) {
127
- throw new JSONParseError(`Failed to parse response, response body: ${text}`, {
128
- cause,
129
- text
396
+ class EventSourceParserStream extends TransformStream {
397
+ constructor({ onError, onRetry, onComment, onId, maxBufferSize } = {}) {
398
+ let parser;
399
+ super({
400
+ start(controller) {
401
+ parser = createParser({
402
+ onEvent: (event) => {
403
+ controller.enqueue(event);
404
+ },
405
+ onError(error) {
406
+ if (typeof onError === "function") {
407
+ onError(error);
408
+ }
409
+ if (onError === "terminate" || error.type === "max-buffer-size-exceeded") {
410
+ controller.error(error);
411
+ }
412
+ },
413
+ onRetry,
414
+ onComment,
415
+ onId,
416
+ maxBufferSize
417
+ });
418
+ },
419
+ transform(chunk) {
420
+ parser.feed(chunk);
421
+ }
130
422
  });
131
423
  }
132
- };
133
-
134
- const trampoline = async (fn) => {
135
- let result = await fn();
136
- while (result instanceof Function)
137
- result = await result();
138
- return result;
139
- };
424
+ }
140
425
 
141
- export { APICallError, InvalidResponseError, InvalidToolCallError, InvalidToolInputError, JSONParseError, RemoteAPIError, ToolExecutionError, XSAIError, clean, objCamelToSnake, postJSON, requestBody, requestHeaders, requestURL, responseCatch, responseJSON, strCamelToSnake, trampoline };
426
+ export { EventSourceParserStream, HttpError, XSAIError, requestURL, sendRequest };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@xsai/shared",
3
3
  "type": "module",
4
- "version": "0.5.1",
5
- "description": "extra-small AI SDK.",
4
+ "version": "1.0.0-beta.2",
5
+ "description": "extra-small AI SDK: shared utilities for xsAI packages.",
6
6
  "author": "Moeru AI",
7
7
  "license": "MIT",
8
8
  "homepage": "https://xsai.js.org",
@@ -14,8 +14,10 @@
14
14
  "bugs": "https://github.com/moeru-ai/xsai/issues",
15
15
  "keywords": [
16
16
  "xsai",
17
+ "ai",
17
18
  "openai",
18
- "ai"
19
+ "shared",
20
+ "utils"
19
21
  ],
20
22
  "sideEffects": false,
21
23
  "exports": {
@@ -28,11 +30,15 @@
28
30
  "files": [
29
31
  "dist"
30
32
  ],
33
+ "engines": {
34
+ "node": ">=26"
35
+ },
31
36
  "devDependencies": {
32
- "@moeru/std": "^0.1.0-beta.20"
37
+ "eventsource-parser": "^4.1.1"
33
38
  },
34
39
  "scripts": {
35
- "build": "pkgroll"
40
+ "build": "pkgroll --clean-dist",
41
+ "test": "vitest run"
36
42
  },
37
43
  "main": "./dist/index.js",
38
44
  "types": "./dist/index.d.ts"