@xsai/stream-text 0.1.0-beta.4 → 0.1.0-beta.6

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/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { FinishReason, Usage, ChatOptions } from '@xsai/shared-chat';
1
+ import { ToolCall, FinishReason, Usage, ChatOptions, Tool, Message, CompletionToolCall, CompletionToolResult, AssistantMessage } from '@xsai/shared-chat';
2
2
 
3
3
  interface StreamTextChunkResult {
4
4
  choices: {
5
5
  delta: {
6
- content: string;
6
+ content?: string;
7
+ refusal?: string;
7
8
  role: 'assistant';
9
+ tool_calls?: ToolCall[];
8
10
  };
9
11
  finish_reason?: FinishReason;
10
12
  index: number;
@@ -16,10 +18,34 @@ interface StreamTextChunkResult {
16
18
  system_fingerprint: string;
17
19
  usage?: Usage;
18
20
  }
21
+ /**
22
+ * Options for configuring the StreamText functionality.
23
+ */
19
24
  interface StreamTextOptions extends ChatOptions {
20
- onChunk?: (chunk: StreamTextChunkResult) => Promise<void> | void;
21
- /** if you want to disable stream, use `@xsai/generate-{text,object}` */
25
+ /** @default 1 */
26
+ maxSteps?: number;
27
+ /**
28
+ * Callback function that is called with each chunk of the stream.
29
+ * @param chunk - The current chunk of the stream.
30
+ */
31
+ onChunk?: (chunk: StreamTextChunkResult) => Promise<unknown> | unknown;
32
+ /**
33
+ * Callback function that is called when the stream is finished.
34
+ * @param result - The final result of the stream.
35
+ */
36
+ onFinish?: (steps: StreamTextStep[]) => Promise<unknown> | unknown;
37
+ /**
38
+ * Callback function that is called when a step in the stream is finished.
39
+ * @param step - The result of the finished step.
40
+ */
41
+ onStepFinish?: (step: StreamTextStep) => Promise<unknown> | unknown;
42
+ /**
43
+ * If you want to disable stream, use `@xsai/generate-{text,object}`.
44
+ */
22
45
  stream?: never;
46
+ /**
47
+ * Options for configuring the stream.
48
+ */
23
49
  streamOptions?: {
24
50
  /**
25
51
  * Return usage.
@@ -28,16 +54,42 @@ interface StreamTextOptions extends ChatOptions {
28
54
  */
29
55
  usage?: boolean;
30
56
  };
57
+ /**
58
+ * List of tools to be used in the stream.
59
+ */
60
+ tools?: Tool[];
31
61
  }
32
62
  interface StreamTextResult {
33
63
  chunkStream: ReadableStream<StreamTextChunkResult>;
34
- finishReason?: FinishReason;
64
+ stepStream: ReadableStream<StreamTextStep>;
35
65
  textStream: ReadableStream<string>;
66
+ }
67
+ interface StreamTextStep {
68
+ choices: StreamTextChoice[];
69
+ messages: Message[];
70
+ toolCalls: CompletionToolCall[];
71
+ toolResults: CompletionToolResult[];
36
72
  usage?: Usage;
37
73
  }
38
- /**
39
- * @experimental WIP, does not support function calling (tools).
40
- */
74
+ /** @internal */
75
+ interface StreamTextChoice {
76
+ finish_reason?: FinishReason | null;
77
+ index: number;
78
+ message: StreamTextMessage;
79
+ }
80
+ /** @internal */
81
+ interface StreamTextMessage extends Omit<AssistantMessage, 'tool_calls'> {
82
+ content?: string;
83
+ tool_calls?: {
84
+ [id: string]: StreamTextToolCall;
85
+ };
86
+ }
87
+ /** @internal */
88
+ interface StreamTextToolCall extends ToolCall {
89
+ function: ToolCall['function'] & {
90
+ parsed_arguments: Record<string, unknown>;
91
+ };
92
+ }
41
93
  declare const streamText: (options: StreamTextOptions) => Promise<StreamTextResult>;
42
94
 
43
- export { type StreamTextChunkResult, type StreamTextOptions, type StreamTextResult, streamText };
95
+ export { type StreamTextChunkResult, type StreamTextOptions, type StreamTextResult, type StreamTextStep, streamText };
package/dist/index.js CHANGED
@@ -1,55 +1,256 @@
1
- import { chat } from '@xsai/shared-chat';
1
+ // ../shared/src/error/index.ts
2
+ var XSAIError = class extends Error {
3
+ response;
4
+ constructor(message, response) {
5
+ super(message);
6
+ this.name = "XSAIError";
7
+ this.response = response;
8
+ }
9
+ };
2
10
 
3
- const chunkHeaderPrefix = "data:";
4
- const streamText = async (options) => chat({
5
- ...options,
6
- stream: true
7
- }).then(async (res) => {
8
- const decoder = new TextDecoder();
9
- let finishReason;
10
- let usage;
11
- const processLine = async (line, controller) => {
12
- if (!line || !line.startsWith(chunkHeaderPrefix))
13
- return;
14
- const content = line.slice(chunkHeaderPrefix.length);
15
- const data = content.startsWith(" ") ? content.slice(1) : content;
16
- if (data === "[DONE]") {
17
- controller.terminate();
18
- return true;
11
+ // src/index.ts
12
+ import { chat } from "@xsai/shared-chat";
13
+
14
+ // src/helper.ts
15
+ var CHUNK_HEADER_PREFIX = "data:";
16
+ var parseChunk = (text) => {
17
+ if (!text || !text.startsWith(CHUNK_HEADER_PREFIX))
18
+ return [void 0, false];
19
+ const content = text.slice(CHUNK_HEADER_PREFIX.length);
20
+ const data = content.startsWith(" ") ? content.slice(1) : content;
21
+ if (data === "[DONE]") {
22
+ return [void 0, true];
23
+ }
24
+ if (data.startsWith("{") && data.includes('"error":')) {
25
+ throw new Error(`Error from server: ${data}`);
26
+ }
27
+ const chunk = JSON.parse(data);
28
+ return [chunk, false];
29
+ };
30
+
31
+ // src/index.ts
32
+ var streamText = async (options) => {
33
+ let chunkCtrl;
34
+ let stepCtrl;
35
+ let textCtrl;
36
+ const chunkStream = new ReadableStream({
37
+ start(controller) {
38
+ chunkCtrl = controller;
19
39
  }
20
- if (data.startsWith("{") && data.includes('"error":')) {
21
- controller.error(new Error(`Error from server: ${data}`));
22
- return true;
40
+ });
41
+ const stepStream = new ReadableStream({
42
+ start(controller) {
43
+ stepCtrl = controller;
23
44
  }
24
- const chunk = JSON.parse(data);
25
- controller.enqueue(chunk);
26
- if (options.onChunk)
27
- await options.onChunk(chunk);
28
- if (chunk.choices[0].finish_reason !== undefined) {
29
- finishReason = chunk.choices[0].finish_reason;
45
+ });
46
+ const textStream = new ReadableStream({
47
+ start(controller) {
48
+ textCtrl = controller;
30
49
  }
31
- if (chunk.usage !== undefined) {
32
- usage = chunk.usage;
50
+ });
51
+ const maxSteps = options.maxSteps ?? 1;
52
+ const decoder = new TextDecoder();
53
+ const steps = [];
54
+ const stepOne = async (options2) => {
55
+ const step = {
56
+ choices: [],
57
+ messages: structuredClone(options2.messages),
58
+ toolCalls: [],
59
+ toolResults: []
60
+ };
61
+ const choiceState = {};
62
+ let buffer = "";
63
+ let shouldOutputText = true;
64
+ const endToolCall = (state, id) => {
65
+ if (state.endedToolCallIDs.has(id)) {
66
+ return;
67
+ }
68
+ const toolCall = step.choices[state.index].message.tool_calls[id];
69
+ try {
70
+ toolCall.function.parsed_arguments = JSON.parse(toolCall.function.arguments);
71
+ } catch (error) {
72
+ state.toolCallErrors[id] = error;
73
+ }
74
+ state.endedToolCallIDs.add(id);
75
+ state.currentToolID = null;
76
+ };
77
+ await chat({
78
+ ...options2,
79
+ stream: true
80
+ }).then(
81
+ async (res) => res.body.pipeThrough(new TransformStream({
82
+ transform: async (chunk, controller) => {
83
+ const text = decoder.decode(chunk, { stream: true });
84
+ buffer += text;
85
+ const lines = buffer.split("\n");
86
+ buffer = lines.pop() ?? "";
87
+ for (const line of lines) {
88
+ try {
89
+ const [chunk2, isEnd] = parseChunk(line);
90
+ if (isEnd)
91
+ break;
92
+ if (chunk2) {
93
+ controller.enqueue(chunk2);
94
+ }
95
+ } catch (error) {
96
+ controller.error(error);
97
+ }
98
+ }
99
+ }
100
+ })).pipeTo(new WritableStream({
101
+ abort: (reason) => {
102
+ chunkCtrl.error(reason);
103
+ stepCtrl.error(reason);
104
+ textCtrl.error(reason);
105
+ },
106
+ // eslint-disable-next-line sonarjs/cognitive-complexity
107
+ write: async (chunk) => {
108
+ options2.onChunk?.(chunk);
109
+ chunkCtrl.enqueue(chunk);
110
+ const choice = chunk.choices[0];
111
+ if (!choice)
112
+ throw new XSAIError("no choice found");
113
+ if (choice.delta.tool_calls) {
114
+ shouldOutputText = false;
115
+ }
116
+ const { delta, finish_reason, index, ...rest } = choice;
117
+ const choiceSnapshot = step.choices[index] ??= {
118
+ finish_reason,
119
+ index,
120
+ message: {
121
+ role: "assistant"
122
+ }
123
+ };
124
+ if (finish_reason !== void 0) {
125
+ choiceSnapshot.finish_reason = finish_reason;
126
+ if (finish_reason === "length") {
127
+ throw new XSAIError("length exceeded");
128
+ }
129
+ if (finish_reason === "content_filter") {
130
+ throw new XSAIError("content filter");
131
+ }
132
+ }
133
+ Object.assign(choiceSnapshot, rest);
134
+ const { content, refusal, tool_calls, ...rests } = delta;
135
+ const message = choiceSnapshot.message;
136
+ Object.assign(message, rests);
137
+ if (refusal !== void 0) {
138
+ message.refusal = (message.refusal || "") + refusal;
139
+ }
140
+ if (content !== void 0) {
141
+ message.content = (message.content || "") + content;
142
+ shouldOutputText && textCtrl?.enqueue(content);
143
+ }
144
+ for (const { function: fn, id, type } of tool_calls || []) {
145
+ message.tool_calls ??= {};
146
+ const toolCall = message.tool_calls[id] ??= {
147
+ function: {
148
+ arguments: "",
149
+ name: fn.name,
150
+ parsed_arguments: {}
151
+ },
152
+ id,
153
+ type
154
+ };
155
+ toolCall.function.arguments += fn.arguments;
156
+ }
157
+ const state = choiceState[index] ??= {
158
+ calledToolCallIDs: /* @__PURE__ */ new Set(),
159
+ currentToolID: null,
160
+ endedToolCallIDs: /* @__PURE__ */ new Set(),
161
+ index,
162
+ toolCallErrors: {},
163
+ toolCallResults: {}
164
+ };
165
+ if (finish_reason) {
166
+ if (state.currentToolID !== null) {
167
+ endToolCall(state, state.currentToolID);
168
+ }
169
+ }
170
+ for (const toolCall of delta.tool_calls || []) {
171
+ if (state.currentToolID !== null && state.currentToolID !== toolCall.id) {
172
+ endToolCall(state, state.currentToolID);
173
+ }
174
+ state.calledToolCallIDs.add(toolCall.id);
175
+ state.currentToolID = toolCall.id;
176
+ }
177
+ }
178
+ }))
179
+ );
180
+ step.messages.push({
181
+ content: step.choices[0]?.message.content ?? "",
182
+ refusal: step.choices[0]?.message.refusal,
183
+ role: "assistant"
184
+ });
185
+ await Promise.allSettled(step.choices.map(async (choice) => {
186
+ const state = choiceState[choice.index];
187
+ return Promise.allSettled([...state.endedToolCallIDs].map(async (id) => {
188
+ const toolCall = choice.message.tool_calls[id];
189
+ step.toolCalls.push({
190
+ args: toolCall.function.arguments,
191
+ toolCallId: id,
192
+ toolCallType: "function",
193
+ toolName: toolCall.function.name
194
+ });
195
+ if (state.toolCallResults[id]) {
196
+ return;
197
+ }
198
+ const tool = options2.tools?.find((tool2) => tool2.function.name === toolCall.function.name);
199
+ if (tool) {
200
+ try {
201
+ const ret = await tool.execute(toolCall.function.parsed_arguments, {
202
+ abortSignal: options2.abortSignal,
203
+ messages: options2.messages,
204
+ toolCallId: id
205
+ });
206
+ state.toolCallResults[id] = ret;
207
+ step.messages.push({
208
+ content: ret,
209
+ role: "tool",
210
+ tool_call_id: id
211
+ });
212
+ step.toolResults.push({
213
+ args: toolCall.function.parsed_arguments,
214
+ result: ret,
215
+ toolCallId: id,
216
+ toolName: toolCall.function.name
217
+ });
218
+ } catch (error) {
219
+ state.toolCallErrors[id] = error;
220
+ }
221
+ } else {
222
+ state.toolCallErrors[id] = new XSAIError(`tool ${toolCall.function.name} not found`);
223
+ }
224
+ }));
225
+ }));
226
+ steps.push(step);
227
+ stepCtrl.enqueue(step);
228
+ options2.onStepFinish?.(step);
229
+ if (shouldOutputText) {
230
+ return;
33
231
  }
232
+ return async () => stepOne({ ...options2, messages: step.messages });
34
233
  };
35
- let buffer = "";
36
- const rawChunkStream = res.body.pipeThrough(new TransformStream({
37
- transform: async (chunk, controller) => {
38
- const text = decoder.decode(chunk, { stream: true });
39
- buffer += text;
40
- const lines = buffer.split("\n");
41
- buffer = lines.pop() ?? "";
42
- for (const line of lines) {
43
- if (await processLine(line, controller))
44
- break;
45
- }
234
+ const invokeFunctionCalls = async () => {
235
+ for (let i = 1, ret = await stepOne(options); typeof ret === "function" && i < maxSteps; ret = await ret(), i += 1) {
236
+ ;
46
237
  }
47
- }));
48
- const [chunkStream, rawTextStream] = rawChunkStream.tee();
49
- const textStream = rawTextStream.pipeThrough(new TransformStream({
50
- transform: (chunk, controller) => controller.enqueue(chunk.choices[0].delta.content)
51
- }));
52
- return { chunkStream, finishReason, textStream, usage };
53
- });
54
-
55
- export { streamText };
238
+ options.onFinish?.(steps);
239
+ chunkCtrl.close();
240
+ stepCtrl.close();
241
+ textCtrl.close();
242
+ };
243
+ void invokeFunctionCalls().catch((error) => {
244
+ chunkCtrl.error(error);
245
+ stepCtrl.error(error);
246
+ textCtrl.error(error);
247
+ });
248
+ return Promise.resolve({
249
+ chunkStream,
250
+ stepStream,
251
+ textStream
252
+ });
253
+ };
254
+ export {
255
+ streamText
256
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xsai/stream-text",
3
3
  "type": "module",
4
- "version": "0.1.0-beta.4",
4
+ "version": "0.1.0-beta.6",
5
5
  "description": "extra-small AI SDK for Browser, Node.js, Deno, Bun or Edge Runtime.",
6
6
  "author": "Moeru AI",
7
7
  "license": "MIT",
@@ -22,10 +22,9 @@
22
22
  ".": {
23
23
  "types": "./dist/index.d.ts",
24
24
  "default": "./dist/index.js"
25
- }
25
+ },
26
+ "./package.json": "./package.json"
26
27
  },
27
- "main": "./dist/index.js",
28
- "types": "./dist/index.d.ts",
29
28
  "files": [
30
29
  "dist"
31
30
  ],
@@ -33,13 +32,15 @@
33
32
  "@xsai/shared-chat": ""
34
33
  },
35
34
  "devDependencies": {
36
- "@xsai/providers": "",
37
- "@xsai/shared": ""
35
+ "@xsai/shared": "",
36
+ "@xsai/tool": "",
37
+ "valibot": "^1.0.0-rc.1"
38
38
  },
39
39
  "scripts": {
40
- "build": "pkgroll",
41
- "build:watch": "pkgroll --watch",
40
+ "build": "tsup",
42
41
  "test": "vitest run",
43
42
  "test:watch": "vitest"
44
- }
43
+ },
44
+ "main": "./dist/index.js",
45
+ "types": "./dist/index.d.ts"
45
46
  }