@xsai/shared 0.5.0 → 1.0.0-beta.1
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 +28 -0
- package/dist/index.d.ts +143 -130
- package/dist/index.js +401 -116
- package/package.json +8 -5
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
|
|
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?:
|
|
95
|
-
|
|
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
|
|
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
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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 {
|
|
144
|
-
export type {
|
|
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
|
-
|
|
3
|
-
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
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
|
-
|
|
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
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 {
|
|
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
|
-
"description": "extra-small AI SDK.",
|
|
4
|
+
"version": "1.0.0-beta.1",
|
|
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
|
-
"
|
|
19
|
+
"shared",
|
|
20
|
+
"utils"
|
|
19
21
|
],
|
|
20
22
|
"sideEffects": false,
|
|
21
23
|
"exports": {
|
|
@@ -29,10 +31,11 @@
|
|
|
29
31
|
"dist"
|
|
30
32
|
],
|
|
31
33
|
"devDependencies": {
|
|
32
|
-
"
|
|
34
|
+
"eventsource-parser": "^4.1.1"
|
|
33
35
|
},
|
|
34
36
|
"scripts": {
|
|
35
|
-
"build": "pkgroll"
|
|
37
|
+
"build": "pkgroll --clean-dist",
|
|
38
|
+
"test": "vitest run"
|
|
36
39
|
},
|
|
37
40
|
"main": "./dist/index.js",
|
|
38
41
|
"types": "./dist/index.d.ts"
|