@xsai/stream-object 0.0.25

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Moeru AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,149 @@
1
+ import { Schema, Infer } from '@typeschema/main';
2
+ import { StreamTextOptions, StreamTextResult } from '@xsai/stream-text';
3
+
4
+ /**
5
+ Matches any [primitive value](https://developer.mozilla.org/en-US/docs/Glossary/Primitive).
6
+
7
+ @category Type
8
+ */
9
+ type Primitive =
10
+ | null
11
+ | undefined
12
+ | string
13
+ | number
14
+ | boolean
15
+ | symbol
16
+ | bigint;
17
+
18
+ declare global {
19
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- It has to be an `interface` so that it can be merged.
20
+ interface SymbolConstructor {
21
+ readonly observable: symbol;
22
+ }
23
+ }
24
+
25
+ /**
26
+ Matches any primitive, `void`, `Date`, or `RegExp` value.
27
+ */
28
+ type BuiltIns = Primitive | void | Date | RegExp;
29
+
30
+ /**
31
+ @see PartialDeep
32
+ */
33
+ type PartialDeepOptions = {
34
+ /**
35
+ Whether to affect the individual elements of arrays and tuples.
36
+
37
+ @default false
38
+ */
39
+ readonly recurseIntoArrays?: boolean;
40
+ };
41
+
42
+ /**
43
+ Create a type from another type with all keys and nested keys set to optional.
44
+
45
+ Use-cases:
46
+ - Merging a default settings/config object with another object, the second object would be a deep partial of the default object.
47
+ - Mocking and testing complex entities, where populating an entire object with its keys would be redundant in terms of the mock or test.
48
+
49
+ @example
50
+ ```
51
+ import type {PartialDeep} from 'type-fest';
52
+
53
+ const settings: Settings = {
54
+ textEditor: {
55
+ fontSize: 14;
56
+ fontColor: '#000000';
57
+ fontWeight: 400;
58
+ }
59
+ autocomplete: false;
60
+ autosave: true;
61
+ };
62
+
63
+ const applySavedSettings = (savedSettings: PartialDeep<Settings>) => {
64
+ return {...settings, ...savedSettings};
65
+ }
66
+
67
+ settings = applySavedSettings({textEditor: {fontWeight: 500}});
68
+ ```
69
+
70
+ By default, this does not affect elements in array and tuple types. You can change this by passing `{recurseIntoArrays: true}` as the second type argument:
71
+
72
+ ```
73
+ import type {PartialDeep} from 'type-fest';
74
+
75
+ interface Settings {
76
+ languages: string[];
77
+ }
78
+
79
+ const partialSettings: PartialDeep<Settings, {recurseIntoArrays: true}> = {
80
+ languages: [undefined]
81
+ };
82
+ ```
83
+
84
+ @category Object
85
+ @category Array
86
+ @category Set
87
+ @category Map
88
+ */
89
+ type PartialDeep<T, Options extends PartialDeepOptions = {}> = T extends BuiltIns | (((...arguments_: any[]) => unknown)) | (new (...arguments_: any[]) => unknown)
90
+ ? T
91
+ : T extends Map<infer KeyType, infer ValueType>
92
+ ? PartialMapDeep<KeyType, ValueType, Options>
93
+ : T extends Set<infer ItemType>
94
+ ? PartialSetDeep<ItemType, Options>
95
+ : T extends ReadonlyMap<infer KeyType, infer ValueType>
96
+ ? PartialReadonlyMapDeep<KeyType, ValueType, Options>
97
+ : T extends ReadonlySet<infer ItemType>
98
+ ? PartialReadonlySetDeep<ItemType, Options>
99
+ : T extends object
100
+ ? T extends ReadonlyArray<infer ItemType> // Test for arrays/tuples, per https://github.com/microsoft/TypeScript/issues/35156
101
+ ? Options['recurseIntoArrays'] extends true
102
+ ? ItemType[] extends T // Test for arrays (non-tuples) specifically
103
+ ? readonly ItemType[] extends T // Differentiate readonly and mutable arrays
104
+ ? ReadonlyArray<PartialDeep<ItemType | undefined, Options>>
105
+ : Array<PartialDeep<ItemType | undefined, Options>>
106
+ : PartialObjectDeep<T, Options> // Tuples behave properly
107
+ : T // If they don't opt into array testing, just use the original type
108
+ : PartialObjectDeep<T, Options>
109
+ : unknown;
110
+
111
+ /**
112
+ Same as `PartialDeep`, but accepts only `Map`s and as inputs. Internal helper for `PartialDeep`.
113
+ */
114
+ type PartialMapDeep<KeyType, ValueType, Options extends PartialDeepOptions> = {} & Map<PartialDeep<KeyType, Options>, PartialDeep<ValueType, Options>>;
115
+
116
+ /**
117
+ Same as `PartialDeep`, but accepts only `Set`s as inputs. Internal helper for `PartialDeep`.
118
+ */
119
+ type PartialSetDeep<T, Options extends PartialDeepOptions> = {} & Set<PartialDeep<T, Options>>;
120
+
121
+ /**
122
+ Same as `PartialDeep`, but accepts only `ReadonlyMap`s as inputs. Internal helper for `PartialDeep`.
123
+ */
124
+ type PartialReadonlyMapDeep<KeyType, ValueType, Options extends PartialDeepOptions> = {} & ReadonlyMap<PartialDeep<KeyType, Options>, PartialDeep<ValueType, Options>>;
125
+
126
+ /**
127
+ Same as `PartialDeep`, but accepts only `ReadonlySet`s as inputs. Internal helper for `PartialDeep`.
128
+ */
129
+ type PartialReadonlySetDeep<T, Options extends PartialDeepOptions> = {} & ReadonlySet<PartialDeep<T, Options>>;
130
+
131
+ /**
132
+ Same as `PartialDeep`, but accepts only `object`s as inputs. Internal helper for `PartialDeep`.
133
+ */
134
+ type PartialObjectDeep<ObjectType extends object, Options extends PartialDeepOptions> = {
135
+ [KeyType in keyof ObjectType]?: PartialDeep<ObjectType[KeyType], Options>
136
+ };
137
+
138
+ interface StreamObjectOptions<T extends Schema> extends StreamTextOptions {
139
+ schema: T;
140
+ schemaDescription?: string;
141
+ schemaName?: string;
142
+ }
143
+ interface StreamObjectResult<T extends Schema> extends StreamTextResult {
144
+ partialObjectStream: ReadableStream<PartialDeep<Infer<T>>>;
145
+ }
146
+ /** @experimental WIP */
147
+ declare const streamObject: <T extends Schema>(options: StreamObjectOptions<T>) => Promise<StreamObjectResult<T>>;
148
+
149
+ export { type StreamObjectOptions, type StreamObjectResult, streamObject as default, streamObject };
package/dist/index.js ADDED
@@ -0,0 +1,271 @@
1
+ import { toJSONSchema } from '@typeschema/main';
2
+ import { clean } from '@xsai/shared';
3
+ import { streamText } from '@xsai/stream-text';
4
+
5
+ var parse = {};
6
+
7
+ (function (exports) {
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.parse = void 0;
10
+ function parse(s) {
11
+ if (s === undefined) {
12
+ return undefined;
13
+ }
14
+ if (s === null) {
15
+ return null;
16
+ }
17
+ if (s === '') {
18
+ return '';
19
+ }
20
+ // remove incomplete escaped characters at the end of the string
21
+ s = s.replace(/\\+$/, match => match.length % 2 === 0 ? match : match.slice(0, -1));
22
+ try {
23
+ return JSON.parse(s);
24
+ }
25
+ catch (e) {
26
+ const [data, reminding] = s.trimLeft()[0] === ':'
27
+ ? parseAny(s, e)
28
+ : parseAny(s, e, parseStringWithoutQuote);
29
+ parse.lastParseReminding = reminding;
30
+ if (parse.onExtraToken && reminding.length > 0) {
31
+ parse.onExtraToken(s, data, reminding);
32
+ }
33
+ return data;
34
+ }
35
+ }
36
+ exports.parse = parse;
37
+ (function (parse) {
38
+ parse.onExtraToken = (text, data, reminding) => {
39
+ console.error('parsed json with extra tokens:', {
40
+ text,
41
+ data,
42
+ reminding,
43
+ });
44
+ };
45
+ })(parse = exports.parse || (exports.parse = {}));
46
+ function parseAny(s, e, fallback) {
47
+ const parser = parsers[s[0]] || fallback;
48
+ if (!parser) {
49
+ console.error(`no parser registered for ${JSON.stringify(s[0])}:`, { s });
50
+ throw e;
51
+ }
52
+ return parser(s, e);
53
+ }
54
+ function parseStringCasual(s, e, delimiters) {
55
+ if (s[0] === '"') {
56
+ return parseString(s);
57
+ }
58
+ if (s[0] === "'") {
59
+ return parseSingleQuoteString(s);
60
+ }
61
+ return parseStringWithoutQuote(s, e, delimiters);
62
+ }
63
+ const parsers = {};
64
+ function skipSpace(s) {
65
+ return s.trimLeft();
66
+ }
67
+ parsers[' '] = parseSpace;
68
+ parsers['\r'] = parseSpace;
69
+ parsers['\n'] = parseSpace;
70
+ parsers['\t'] = parseSpace;
71
+ function parseSpace(s, e) {
72
+ s = skipSpace(s);
73
+ return parseAny(s, e);
74
+ }
75
+ parsers['['] = parseArray;
76
+ function parseArray(s, e) {
77
+ s = s.substr(1); // skip starting '['
78
+ const acc = [];
79
+ s = skipSpace(s);
80
+ for (; s.length > 0;) {
81
+ if (s[0] === ']') {
82
+ s = s.substr(1); // skip ending ']'
83
+ break;
84
+ }
85
+ const res = parseAny(s, e, (s, e) => parseStringWithoutQuote(s, e, [',', ']']));
86
+ acc.push(res[0]);
87
+ s = res[1];
88
+ s = skipSpace(s);
89
+ if (s[0] === ',') {
90
+ s = s.substring(1);
91
+ s = skipSpace(s);
92
+ }
93
+ }
94
+ return [acc, s];
95
+ }
96
+ for (const c of '0123456789.-'.slice()) {
97
+ parsers[c] = parseNumber;
98
+ }
99
+ function parseNumber(s) {
100
+ for (let i = 0; i < s.length; i++) {
101
+ const c = s[i];
102
+ if (parsers[c] === parseNumber) {
103
+ continue;
104
+ }
105
+ const num = s.substring(0, i);
106
+ s = s.substring(i);
107
+ return [numToStr(num), s];
108
+ }
109
+ return [numToStr(s), ''];
110
+ }
111
+ function numToStr(s) {
112
+ if (s === '-') {
113
+ return -0;
114
+ }
115
+ const num = +s;
116
+ if (Number.isNaN(num)) {
117
+ return s;
118
+ }
119
+ return num;
120
+ }
121
+ parsers['"'] = parseString;
122
+ function parseString(s) {
123
+ for (let i = 1; i < s.length; i++) {
124
+ const c = s[i];
125
+ if (c === '\\') {
126
+ i++;
127
+ continue;
128
+ }
129
+ if (c === '"') {
130
+ const str = fixEscapedCharacters(s.substring(0, i + 1));
131
+ s = s.substring(i + 1);
132
+ return [JSON.parse(str), s];
133
+ }
134
+ }
135
+ return [JSON.parse(fixEscapedCharacters(s) + '"'), ''];
136
+ }
137
+ function fixEscapedCharacters(s) {
138
+ return s.replace(/\n/g, '\\n').replace(/\t/g, '\\t').replace(/\r/g, '\\r');
139
+ }
140
+ parsers["'"] = parseSingleQuoteString;
141
+ function parseSingleQuoteString(s) {
142
+ for (let i = 1; i < s.length; i++) {
143
+ const c = s[i];
144
+ if (c === '\\') {
145
+ i++;
146
+ continue;
147
+ }
148
+ if (c === "'") {
149
+ const str = fixEscapedCharacters(s.substring(0, i + 1));
150
+ s = s.substring(i + 1);
151
+ return [JSON.parse('"' + str.slice(1, -1) + '"'), s];
152
+ }
153
+ }
154
+ return [JSON.parse('"' + fixEscapedCharacters(s.slice(1)) + '"'), ''];
155
+ }
156
+ function parseStringWithoutQuote(s, e, delimiters = [' ']) {
157
+ const index = Math.min(...delimiters.map(delimiter => {
158
+ const index = s.indexOf(delimiter);
159
+ return index === -1 ? s.length : index;
160
+ }));
161
+ const value = s.substring(0, index).trim();
162
+ const rest = s.substring(index);
163
+ return [value, rest];
164
+ }
165
+ parsers['{'] = parseObject;
166
+ function parseObject(s, e) {
167
+ s = s.substr(1); // skip starting '{'
168
+ const acc = {};
169
+ s = skipSpace(s);
170
+ for (; s.length > 0;) {
171
+ if (s[0] === '}') {
172
+ s = s.substr(1); // skip ending '}'
173
+ break;
174
+ }
175
+ const keyRes = parseStringCasual(s, e, [':', '}']);
176
+ const key = keyRes[0];
177
+ s = keyRes[1];
178
+ s = skipSpace(s);
179
+ if (s[0] !== ':') {
180
+ acc[key] = undefined;
181
+ break;
182
+ }
183
+ s = s.substr(1); // skip ':'
184
+ s = skipSpace(s);
185
+ if (s.length === 0) {
186
+ acc[key] = undefined;
187
+ break;
188
+ }
189
+ const valueRes = parseAny(s, e);
190
+ acc[key] = valueRes[0];
191
+ s = valueRes[1];
192
+ s = skipSpace(s);
193
+ if (s[0] === ',') {
194
+ s = s.substr(1);
195
+ s = skipSpace(s);
196
+ }
197
+ }
198
+ return [acc, s];
199
+ }
200
+ parsers['t'] = parseTrue;
201
+ function parseTrue(s, e) {
202
+ return parseToken(s, `true`, true, e);
203
+ }
204
+ parsers['f'] = parseFalse;
205
+ function parseFalse(s, e) {
206
+ return parseToken(s, `false`, false, e);
207
+ }
208
+ parsers['n'] = parseNull;
209
+ function parseNull(s, e) {
210
+ return parseToken(s, `null`, null, e);
211
+ }
212
+ function parseToken(s, tokenStr, tokenVal, e) {
213
+ for (let i = tokenStr.length; i >= 1; i--) {
214
+ if (s.startsWith(tokenStr.slice(0, i))) {
215
+ return [tokenVal, s.slice(i)];
216
+ }
217
+ }
218
+ /* istanbul ignore next */
219
+ {
220
+ const prefix = JSON.stringify(s.slice(0, tokenStr.length));
221
+ console.error(`unknown token starting with ${prefix}:`, { s });
222
+ throw e;
223
+ }
224
+ }
225
+
226
+ } (parse));
227
+
228
+ const streamObject = async (options) => await streamText({
229
+ ...options,
230
+ response_format: {
231
+ json_schema: {
232
+ description: options.schemaDescription,
233
+ name: options.schemaName ?? "json_schema",
234
+ schema: await toJSONSchema(options.schema).then((json) => clean({
235
+ ...json,
236
+ $schema: void 0
237
+ })),
238
+ strict: true
239
+ },
240
+ type: "json_schema"
241
+ },
242
+ schema: void 0,
243
+ schemaDescription: void 0,
244
+ schemaName: void 0
245
+ }).then(({ chunkStream, finishReason, textStream: rawTextStream, usage }) => {
246
+ const [textStream, rawPartialObjectStream] = rawTextStream.tee();
247
+ let partialObjectData = "";
248
+ let partialObjectSnapshot = {};
249
+ const partialObjectStream = rawPartialObjectStream.pipeThrough(new TransformStream({
250
+ transform: (chunk, controller) => {
251
+ partialObjectData += chunk;
252
+ try {
253
+ const data = parse.parse(partialObjectData);
254
+ if (JSON.stringify(partialObjectSnapshot) !== JSON.stringify(data)) {
255
+ partialObjectSnapshot = data;
256
+ controller.enqueue(data);
257
+ }
258
+ } catch {
259
+ }
260
+ }
261
+ }));
262
+ return {
263
+ chunkStream,
264
+ finishReason,
265
+ partialObjectStream,
266
+ textStream,
267
+ usage
268
+ };
269
+ });
270
+
271
+ export { streamObject as default, streamObject };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@xsai/stream-object",
3
+ "version": "0.0.25",
4
+ "type": "module",
5
+ "author": "Moeru AI",
6
+ "license": "MIT",
7
+ "homepage": "https://xsai.js.org",
8
+ "description": "extra-small AI SDK for Browser, Node.js, Deno, Bun or Edge Runtime.",
9
+ "keywords": [
10
+ "xsai",
11
+ "openai",
12
+ "ai"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/moeru-ai/xsai.git",
17
+ "directory": "packages/stream-object"
18
+ },
19
+ "bugs": "https://github.com/moeru-ai/xsai/issues",
20
+ "sideEffects": false,
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "dependencies": {
33
+ "@typeschema/main": "^0.14.1",
34
+ "@xsai/shared": "",
35
+ "@xsai/stream-text": ""
36
+ },
37
+ "devDependencies": {
38
+ "@gcornut/valibot-json-schema": "^0.42.0",
39
+ "@xsai/providers": "",
40
+ "best-effort-json-parser": "^1.1.2",
41
+ "type-fest": "^4.31.0",
42
+ "valibot": "^0.42.1"
43
+ },
44
+ "scripts": {
45
+ "build": "pkgroll",
46
+ "build:watch": "pkgroll --watch",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest"
49
+ }
50
+ }