@xsai/stream-text 0.3.0-beta.8 → 0.3.0

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,85 +1,40 @@
1
- import { ToolMessagePart, FinishReason, Usage, ToolCall, ChatOptions, AssistantMessage, Message, CompletionStepType, CompletionToolCall, CompletionToolResult } from '@xsai/shared-chat';
1
+ import { CompletionToolCall, CompletionToolResult, FinishReason, Usage, ChatOptions, CompletionStep, Message } from '@xsai/shared-chat';
2
2
 
3
- type StreamTextEvent = {
3
+ type StreamTextEvent = (CompletionToolCall & {
4
+ type: 'tool-call';
5
+ }) | (CompletionToolResult & {
6
+ type: 'tool-result';
7
+ }) | {
8
+ argsTextDelta: string;
9
+ toolCallId: string;
10
+ toolName: string;
11
+ type: 'tool-call-delta';
12
+ } | {
4
13
  error: unknown;
5
14
  type: 'error';
6
15
  } | {
7
- error?: unknown;
8
- id: string;
9
- result?: string | ToolMessagePart[];
10
- type: 'tool-call-result';
11
- } | {
12
- finishReason?: FinishReason;
16
+ finishReason: FinishReason;
13
17
  type: 'finish';
14
18
  usage?: Usage;
15
- } | {
16
- reasoning: string;
17
- type: 'reasoning';
18
- } | {
19
- refusal: string;
20
- type: 'refusal';
21
19
  } | {
22
20
  text: string;
23
21
  type: 'text-delta';
24
22
  } | {
25
- toolCall: ToolCall;
26
- type: 'tool-call';
27
- } | {
28
- toolCall: ToolCall;
29
- type: 'tool-call-delta';
23
+ toolCallId: string;
24
+ toolName: string;
25
+ type: 'tool-call-streaming-start';
30
26
  };
31
27
 
32
- interface StreamTextChunkResult {
33
- choices: {
34
- delta: {
35
- content?: string;
36
- refusal?: string;
37
- role: 'assistant';
38
- tool_calls?: ToolCall[];
39
- };
40
- finish_reason?: FinishReason;
41
- index: number;
42
- }[];
43
- created: number;
44
- id: string;
45
- model: string;
46
- object: 'chat.completion.chunk';
47
- system_fingerprint: string;
48
- usage?: Usage;
49
- }
50
- /**
51
- * Options for configuring the StreamText functionality.
52
- */
53
28
  interface StreamTextOptions extends ChatOptions {
54
29
  /** @default 1 */
55
30
  maxSteps?: number;
56
- /**
57
- * Callback function that is called with each chunk of the stream.
58
- * @param chunk - The current chunk of the stream.
59
- */
60
- onChunk?: (chunk: StreamTextChunkResult) => Promise<unknown> | unknown;
61
- /**
62
- * Callback function that is called with each event of the stream.
63
- * @param event - The current event of the stream.
64
- */
65
31
  onEvent?: (event: StreamTextEvent) => Promise<unknown> | unknown;
66
- /**
67
- * Callback function that is called when the stream is finished.
68
- * @param result - The final result of the stream.
69
- */
70
- onFinish?: (steps: StreamTextStep[]) => Promise<unknown> | unknown;
71
- /**
72
- * Callback function that is called when a step in the stream is finished.
73
- * @param step - The result of the finished step.
74
- */
75
- onStepFinish?: (step: StreamTextStep) => Promise<unknown> | unknown;
32
+ onFinish?: (step?: CompletionStep) => Promise<unknown> | unknown;
33
+ onStepFinish?: (step: CompletionStep) => Promise<unknown> | unknown;
76
34
  /**
77
35
  * If you want to disable stream, use `@xsai/generate-{text,object}`.
78
36
  */
79
37
  stream?: never;
80
- /**
81
- * Options for configuring the stream.
82
- */
83
38
  streamOptions?: {
84
39
  /**
85
40
  * Return usage.
@@ -89,39 +44,12 @@ interface StreamTextOptions extends ChatOptions {
89
44
  };
90
45
  }
91
46
  interface StreamTextResult {
92
- chunkStream: ReadableStream<StreamTextChunkResult>;
93
- stepStream: ReadableStream<StreamTextStep>;
47
+ fullStream: ReadableStream<StreamTextEvent>;
48
+ messages: Promise<Message[]>;
49
+ steps: Promise<CompletionStep[]>;
94
50
  textStream: ReadableStream<string>;
95
- }
96
- interface StreamTextStep {
97
- choices: StreamTextChoice[];
98
- finishReason: FinishReason;
99
- messages: Message[];
100
- stepType: CompletionStepType;
101
- toolCalls: CompletionToolCall[];
102
- toolResults: CompletionToolResult[];
103
- usage?: Usage;
104
- }
105
- /** @internal */
106
- interface StreamTextChoice {
107
- finishReason?: FinishReason | null;
108
- index: number;
109
- message: StreamTextMessage;
110
- }
111
- /** @internal */
112
- interface StreamTextMessage extends Omit<AssistantMessage, 'tool_calls'> {
113
- content?: string;
114
- toolCalls?: {
115
- [id: number]: StreamTextToolCall;
116
- };
117
- }
118
- /** @internal */
119
- interface StreamTextToolCall extends ToolCall {
120
- function: ToolCall['function'] & {
121
- parsedArguments: Record<string, unknown>;
122
- };
123
- index: number;
51
+ usage: Promise<undefined | Usage>;
124
52
  }
125
53
  declare const streamText: (options: StreamTextOptions) => Promise<StreamTextResult>;
126
54
 
127
- export { type StreamTextChunkResult, type StreamTextEvent, type StreamTextOptions, type StreamTextResult, type StreamTextStep, streamText };
55
+ export { type StreamTextOptions, type StreamTextResult, streamText };
package/dist/index.js CHANGED
@@ -1,20 +1,52 @@
1
1
  import { chat, executeTool, determineStepType } from '@xsai/shared-chat';
2
- import { o as objCamelToSnake } from './case-PdS00lUs.js';
3
2
 
4
- class XSAIError extends Error {
5
- response;
6
- constructor(message, response) {
7
- super(message);
8
- this.name = "XSAIError";
9
- this.response = response;
3
+ const strCamelToSnake = (str) => str.replace(/[A-Z]/g, (s) => `_${s.toLowerCase()}`);
4
+ const objCamelToSnake = (obj) => Object.fromEntries(Object.entries(obj).map(([k, v]) => [strCamelToSnake(k), v]));
5
+
6
+ const trampoline = async (fn) => {
7
+ let result = await fn();
8
+ while (result instanceof Function)
9
+ result = await result();
10
+ return result;
11
+ };
12
+
13
+ class DelayedPromise {
14
+ get promise() {
15
+ if (this._promise == null) {
16
+ this._promise = new Promise((resolve, reject) => {
17
+ if (this.status.type === "resolved") {
18
+ resolve(this.status.value);
19
+ } else if (this.status.type === "rejected") {
20
+ reject(this.status.error);
21
+ }
22
+ this._resolve = resolve;
23
+ this._reject = reject;
24
+ });
25
+ }
26
+ return this._promise;
27
+ }
28
+ _promise;
29
+ _reject;
30
+ _resolve;
31
+ status = { type: "pending" };
32
+ reject(error) {
33
+ this.status = { error, type: "rejected" };
34
+ if (this._promise) {
35
+ this._reject?.(error);
36
+ }
37
+ }
38
+ resolve(value) {
39
+ this.status = { type: "resolved", value };
40
+ if (this._promise) {
41
+ this._resolve?.(value);
42
+ }
10
43
  }
11
44
  }
12
45
 
13
- const CHUNK_HEADER_PREFIX = "data:";
14
46
  const parseChunk = (text) => {
15
- if (!text || !text.startsWith(CHUNK_HEADER_PREFIX))
47
+ if (!text || !text.startsWith("data:"))
16
48
  return [void 0, false];
17
- const content = text.slice(CHUNK_HEADER_PREFIX.length);
49
+ const content = text.slice("data:".length);
18
50
  const data = content.startsWith(" ") ? content.slice(1) : content;
19
51
  if (data === "[DONE]") {
20
52
  return [void 0, true];
@@ -25,258 +57,166 @@ const parseChunk = (text) => {
25
57
  const chunk = JSON.parse(data);
26
58
  return [chunk, false];
27
59
  };
60
+ const transformChunk = () => {
61
+ const decoder = new TextDecoder();
62
+ let buffer = "";
63
+ return new TransformStream({
64
+ transform: async (chunk, controller) => {
65
+ const text = decoder.decode(chunk, { stream: true });
66
+ buffer += text;
67
+ const lines = buffer.split("\n");
68
+ buffer = lines.pop() ?? "";
69
+ for (const line of lines) {
70
+ try {
71
+ const [chunk2, isEnd] = parseChunk(line);
72
+ if (isEnd)
73
+ break;
74
+ if (chunk2) {
75
+ controller.enqueue(chunk2);
76
+ }
77
+ } catch (error) {
78
+ controller.error(error);
79
+ }
80
+ }
81
+ }
82
+ });
83
+ };
28
84
 
29
85
  const streamText = async (options) => {
30
- let chunkCtrl;
31
- let stepCtrl;
32
- let textCtrl;
33
- const chunkStream = new ReadableStream({
34
- start: (controller) => chunkCtrl = controller
35
- });
36
- const stepStream = new ReadableStream({
37
- start: (controller) => stepCtrl = controller
38
- });
39
- const textStream = new ReadableStream({
40
- start: (controller) => textCtrl = controller
41
- });
42
- const maxSteps = options.maxSteps ?? 1;
43
- const decoder = new TextDecoder();
44
86
  const steps = [];
45
- const stepOne = async (options2) => {
46
- const step = {
47
- choices: [],
48
- finishReason: "error",
49
- messages: structuredClone(options2.messages),
50
- stepType: "initial",
51
- toolCalls: [],
52
- toolResults: []
87
+ const messages = structuredClone(options.messages);
88
+ const maxSteps = options.maxSteps ?? 1;
89
+ let usage;
90
+ const resultSteps = new DelayedPromise();
91
+ const resultMessages = new DelayedPromise();
92
+ const resultUsage = new DelayedPromise();
93
+ let eventCtrl;
94
+ let textCtrl;
95
+ const eventStream = new ReadableStream({ start: (controller) => eventCtrl = controller });
96
+ const textStream = new ReadableStream({ start: (controller) => textCtrl = controller });
97
+ const pushEvent = (stepEvent) => {
98
+ eventCtrl?.enqueue(stepEvent);
99
+ void options.onEvent?.(stepEvent);
100
+ };
101
+ const pushStep = (step) => {
102
+ steps.push(step);
103
+ void options.onStepFinish?.(step);
104
+ };
105
+ const startStream = async () => {
106
+ const pushUsage = (u) => {
107
+ usage = u;
53
108
  };
54
- const choiceState = {};
55
- let buffer = "";
56
- let finishReason;
57
- let usage;
58
- let shouldOutputText = true;
59
- const endToolCallByIndex = (state, idx) => {
60
- if (state.endedToolCallIndex.has(idx)) {
61
- return;
62
- }
63
- state.endedToolCallIndex.add(idx);
64
- state.currentToolIndex = null;
109
+ let text = "";
110
+ const pushText = (content) => {
111
+ textCtrl?.enqueue(content);
112
+ text += content;
65
113
  };
114
+ const tool_calls = [];
115
+ const toolCalls = [];
116
+ const toolResults = [];
117
+ let finishReason = "other";
66
118
  await chat({
67
- ...options2,
119
+ ...options,
68
120
  maxSteps: void 0,
121
+ messages,
69
122
  stream: true,
70
- streamOptions: options2.streamOptions != null ? objCamelToSnake(options2.streamOptions) : void 0
123
+ streamOptions: options.streamOptions != null ? objCamelToSnake(options.streamOptions) : void 0
71
124
  }).then(
72
- async (res) => res.body.pipeThrough(new TransformStream({
73
- transform: async (chunk, controller) => {
74
- const text = decoder.decode(chunk, { stream: true });
75
- buffer += text;
76
- const lines = buffer.split("\n");
77
- buffer = lines.pop() ?? "";
78
- for (const line of lines) {
79
- try {
80
- const [chunk2, isEnd] = parseChunk(line);
81
- if (isEnd)
82
- break;
83
- if (chunk2) {
84
- controller.enqueue(chunk2);
85
- }
86
- } catch (error) {
87
- controller.error(error);
88
- }
89
- }
90
- }
91
- })).pipeTo(new WritableStream({
125
+ async (res) => res.body.pipeThrough(transformChunk()).pipeTo(new WritableStream({
92
126
  abort: (reason) => {
93
- chunkCtrl.error(reason);
94
- stepCtrl.error(reason);
95
- textCtrl.error(reason);
127
+ eventCtrl?.error(reason);
128
+ textCtrl?.error(reason);
96
129
  },
97
130
  close: () => {
98
- options2.onEvent?.({
99
- finishReason,
100
- type: "finish",
101
- usage
102
- });
103
131
  },
104
- // eslint-disable-next-line sonarjs/cognitive-complexity
105
- write: async (chunk) => {
106
- options2.onChunk?.(chunk);
107
- chunkCtrl.enqueue(chunk);
108
- usage = chunk.usage;
132
+ write: (chunk) => {
133
+ if (chunk.usage)
134
+ pushUsage(chunk.usage);
109
135
  if (chunk.choices == null || chunk.choices.length === 0)
110
136
  return;
111
137
  const choice = chunk.choices[0];
112
- if (choice.delta.tool_calls) {
113
- shouldOutputText = false;
114
- }
115
- const { delta, finish_reason, index, ...rest } = choice;
116
- const choiceSnapshot = step.choices[index] ??= {
117
- finishReason: finish_reason,
118
- index,
119
- message: {
120
- role: "assistant"
121
- }
122
- };
123
- if (finish_reason !== void 0) {
124
- finishReason = finish_reason;
125
- step.finishReason = finish_reason;
126
- choiceSnapshot.finishReason = finish_reason;
127
- if (finish_reason === "length") {
128
- throw new XSAIError("length exceeded");
138
+ if (choice.finish_reason != null)
139
+ finishReason = choice.finish_reason;
140
+ if (choice.delta.tool_calls?.length === 0 || choice.delta.tool_calls == null) {
141
+ if (choice.delta.content != null) {
142
+ pushEvent({ text: choice.delta.content, type: "text-delta" });
143
+ pushText(choice.delta.content);
144
+ } else if (choice.delta.refusal != null) {
145
+ pushEvent({ error: choice.delta.refusal, type: "error" });
146
+ } else if (choice.finish_reason != null) {
147
+ pushEvent({ finishReason: choice.finish_reason, type: "finish", usage });
129
148
  }
130
- if (finish_reason === "content_filter") {
131
- throw new XSAIError("content filter");
132
- }
133
- }
134
- Object.assign(choiceSnapshot, rest);
135
- const { content, refusal, tool_calls, ...rests } = delta;
136
- const message = choiceSnapshot.message;
137
- Object.assign(message, rests);
138
- if (refusal !== void 0) {
139
- message.refusal = (message.refusal || "") + (refusal || "");
140
- options2.onEvent?.({
141
- refusal: message.refusal,
142
- type: "refusal"
143
- });
144
- }
145
- if (content !== void 0) {
146
- message.content = (message.content || "") + (content || "");
147
- shouldOutputText && textCtrl?.enqueue(content);
148
- options2.onEvent?.({
149
- text: content,
150
- type: "text-delta"
151
- });
152
- }
153
- for (const tool_call of tool_calls || []) {
154
- options2.onEvent?.({
155
- toolCall: tool_call,
156
- type: "tool-call-delta"
157
- });
158
- const { function: fn, id, index: index2, type } = tool_call;
159
- message.toolCalls ??= {};
160
- const toolCall = message.toolCalls[index2] ??= {
161
- function: {
162
- arguments: "",
163
- name: fn.name,
164
- parsedArguments: {}
165
- },
166
- id,
167
- index: index2,
168
- type
169
- };
170
- toolCall.function.arguments += fn.arguments;
171
- }
172
- const state = choiceState[index] ??= {
173
- calledToolCallIndex: /* @__PURE__ */ new Set(),
174
- currentToolIndex: null,
175
- endedToolCallIndex: /* @__PURE__ */ new Set(),
176
- index,
177
- toolCallErrors: {},
178
- toolCallResults: {}
179
- };
180
- if (finish_reason) {
181
- if (state.currentToolIndex !== null) {
182
- endToolCallByIndex(state, state.currentToolIndex);
183
- }
184
- }
185
- for (const toolCall of delta.tool_calls || []) {
186
- if (state.currentToolIndex !== toolCall.index && state.currentToolIndex !== null) {
187
- endToolCallByIndex(state, state.currentToolIndex);
149
+ } else {
150
+ for (const toolCall of choice.delta.tool_calls) {
151
+ const { index } = toolCall;
152
+ if (!tool_calls.at(index)) {
153
+ tool_calls[index] = toolCall;
154
+ pushEvent({ toolCallId: toolCall.id, toolName: toolCall.function.name, type: "tool-call-streaming-start" });
155
+ } else {
156
+ tool_calls[index].function.arguments += toolCall.function.arguments;
157
+ pushEvent({ argsTextDelta: toolCall.function.arguments, toolCallId: toolCall.id, toolName: toolCall.function.name, type: "tool-call-delta" });
158
+ }
188
159
  }
189
- state.calledToolCallIndex.add(toolCall.index);
190
- state.currentToolIndex = toolCall.index;
191
160
  }
192
161
  }
193
162
  }))
194
163
  );
195
- step.messages.push({
196
- content: step.choices[0]?.message.content ?? "",
197
- refusal: step.choices[0]?.message.refusal,
198
- role: "assistant",
199
- tool_calls: Object.values(step.choices[0]?.message.toolCalls ?? {}).map((toolCall) => ({
200
- function: {
201
- arguments: toolCall.function.arguments,
202
- name: toolCall.function.name
203
- },
204
- id: toolCall.id,
205
- index: toolCall.index,
206
- type: toolCall.type
207
- }))
208
- });
209
- await Promise.allSettled(step.choices.map(async (choice) => {
210
- const state = choiceState[choice.index];
211
- return Promise.allSettled([...state.endedToolCallIndex].map(async (idx) => {
212
- const toolCall = choice.message.toolCalls[idx];
213
- step.toolCalls.push({
214
- args: toolCall.function.arguments,
215
- toolCallId: toolCall.id,
216
- toolCallType: "function",
217
- toolName: toolCall.function.name
218
- });
219
- if (state.toolCallResults[toolCall.id]) {
220
- return;
221
- }
222
- options2.onEvent?.({
164
+ messages.push({ content: text, role: "assistant", tool_calls });
165
+ if (tool_calls.length !== 0) {
166
+ for (const toolCall of tool_calls) {
167
+ const { completionToolCall, completionToolResult, message } = await executeTool({
168
+ abortSignal: options.abortSignal,
169
+ messages,
223
170
  toolCall,
224
- type: "tool-call"
171
+ tools: options.tools
225
172
  });
226
- try {
227
- const { completionToolResult, message, parsedArgs, result } = await executeTool({
228
- abortSignal: options2.abortSignal,
229
- messages: options2.messages,
230
- toolCall,
231
- tools: options2.tools
232
- });
233
- toolCall.function.parsedArguments = parsedArgs;
234
- state.toolCallResults[toolCall.id] = result;
235
- step.messages.push(message);
236
- step.toolResults.push(completionToolResult);
237
- options2.onEvent?.({
238
- id: toolCall.id,
239
- result,
240
- type: "tool-call-result"
241
- });
242
- } catch (error) {
243
- state.toolCallErrors[idx] = error;
244
- }
245
- }));
246
- }));
247
- step.stepType = determineStepType({
248
- finishReason: step.finishReason,
249
- maxSteps,
250
- stepsLength: steps.length,
251
- toolCallsLength: step.toolCalls.length
252
- });
253
- steps.push(step);
254
- stepCtrl.enqueue(step);
255
- options2.onStepFinish?.(step);
256
- if (shouldOutputText) {
257
- return;
173
+ toolCalls.push(completionToolCall);
174
+ toolResults.push(completionToolResult);
175
+ messages.push(message);
176
+ pushEvent({ ...completionToolCall, type: "tool-call" });
177
+ pushEvent({ ...completionToolResult, type: "tool-result" });
178
+ }
179
+ } else {
180
+ pushEvent({
181
+ finishReason,
182
+ type: "finish",
183
+ usage
184
+ });
258
185
  }
259
- return async () => stepOne({ ...options2, messages: step.messages });
186
+ pushStep({
187
+ finishReason,
188
+ stepType: determineStepType({ finishReason, maxSteps, stepsLength: steps.length, toolCallsLength: toolCalls.length }),
189
+ text,
190
+ toolCalls,
191
+ toolResults,
192
+ usage
193
+ });
194
+ if (toolCalls.length !== 0 && steps.length < maxSteps)
195
+ return async () => startStream();
260
196
  };
261
- const invokeFunctionCalls = async () => {
262
- let ret = await stepOne(options);
263
- while (typeof ret === "function" && steps.length < maxSteps)
264
- ret = await ret();
265
- options.onFinish?.(steps);
266
- chunkCtrl.close();
267
- stepCtrl.close();
268
- textCtrl.close();
197
+ try {
198
+ await trampoline(async () => startStream());
199
+ } catch (err) {
200
+ eventCtrl?.error(err);
201
+ textCtrl?.error(err);
202
+ resultSteps.reject(err);
203
+ resultMessages.reject(err);
204
+ resultUsage.reject(err);
205
+ } finally {
206
+ eventCtrl?.close();
207
+ textCtrl?.close();
208
+ resultSteps.resolve(steps);
209
+ resultMessages.resolve(messages);
210
+ resultUsage.resolve(usage);
211
+ void options.onFinish?.(steps.at(-1));
212
+ }
213
+ return {
214
+ fullStream: eventStream,
215
+ messages: resultMessages.promise,
216
+ steps: resultSteps.promise,
217
+ textStream,
218
+ usage: resultUsage.promise
269
219
  };
270
- void invokeFunctionCalls().catch((error) => {
271
- chunkCtrl.error(error);
272
- stepCtrl.error(error);
273
- textCtrl.error(error);
274
- });
275
- return Promise.resolve({
276
- chunkStream,
277
- stepStream,
278
- textStream
279
- });
280
220
  };
281
221
 
282
222
  export { streamText };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xsai/stream-text",
3
3
  "type": "module",
4
- "version": "0.3.0-beta.8",
4
+ "version": "0.3.0",
5
5
  "description": "extra-small AI SDK.",
6
6
  "author": "Moeru AI",
7
7
  "license": "MIT",
@@ -23,22 +23,18 @@
23
23
  "types": "./dist/index.d.ts",
24
24
  "default": "./dist/index.js"
25
25
  },
26
- "./experimental": {
27
- "types": "./dist/experimental/index.d.ts",
28
- "default": "./dist/experimental/index.js"
29
- },
30
26
  "./package.json": "./package.json"
31
27
  },
32
28
  "files": [
33
29
  "dist"
34
30
  ],
35
31
  "dependencies": {
36
- "@xsai/shared-chat": "~0.3.0-beta.8"
32
+ "@xsai/shared-chat": "~0.3.0"
37
33
  },
38
34
  "devDependencies": {
39
35
  "valibot": "^1.0.0",
40
- "@xsai/shared": "~0.3.0-beta.8",
41
- "@xsai/tool": "~0.3.0-beta.8"
36
+ "@xsai/shared": "~0.3.0",
37
+ "@xsai/tool": "~0.3.0"
42
38
  },
43
39
  "scripts": {
44
40
  "build": "pkgroll",
@@ -1,4 +0,0 @@
1
- const strCamelToSnake = (str) => str.replace(/[A-Z]/g, (s) => `_${s.toLowerCase()}`);
2
- const objCamelToSnake = (obj) => Object.fromEntries(Object.entries(obj).map(([k, v]) => [strCamelToSnake(k), v]));
3
-
4
- export { objCamelToSnake as o };
@@ -1,55 +0,0 @@
1
- import { CompletionToolCall, CompletionToolResult, FinishReason, Usage, ChatOptions, CompletionStep, Message } from '@xsai/shared-chat';
2
-
3
- type StreamTextEvent = (CompletionToolCall & {
4
- type: 'tool-call';
5
- }) | (CompletionToolResult & {
6
- type: 'tool-result';
7
- }) | {
8
- argsTextDelta: string;
9
- toolCallId: string;
10
- toolName: string;
11
- type: 'tool-call-delta';
12
- } | {
13
- error: unknown;
14
- type: 'error';
15
- } | {
16
- finishReason: FinishReason;
17
- type: 'finish';
18
- usage?: Usage;
19
- } | {
20
- text: string;
21
- type: 'text-delta';
22
- } | {
23
- toolCallId: string;
24
- toolName: string;
25
- type: 'tool-call-streaming-start';
26
- };
27
-
28
- interface StreamTextOptions extends ChatOptions {
29
- /** @default 1 */
30
- maxSteps?: number;
31
- onEvent?: (event: StreamTextEvent) => Promise<unknown> | unknown;
32
- onFinish?: (step?: CompletionStep) => Promise<unknown> | unknown;
33
- onStepFinish?: (step: CompletionStep) => Promise<unknown> | unknown;
34
- /**
35
- * If you want to disable stream, use `@xsai/generate-{text,object}`.
36
- */
37
- stream?: never;
38
- streamOptions?: {
39
- /**
40
- * Return usage.
41
- * @default `undefined`
42
- */
43
- includeUsage?: boolean;
44
- };
45
- }
46
- interface StreamTextResult {
47
- fullStream: ReadableStream<StreamTextEvent>;
48
- messages: Promise<Message[]>;
49
- steps: Promise<CompletionStep[]>;
50
- textStream: ReadableStream<string>;
51
- usage: Promise<undefined | Usage>;
52
- }
53
- declare const streamText: (options: StreamTextOptions) => Promise<StreamTextResult>;
54
-
55
- export { type StreamTextOptions, type StreamTextResult, streamText };
@@ -1,215 +0,0 @@
1
- import { chat, executeTool, determineStepType } from '@xsai/shared-chat';
2
- import { o as objCamelToSnake } from '../case-PdS00lUs.js';
3
-
4
- const trampoline = async (fn) => {
5
- let result = await fn();
6
- while (result instanceof Function)
7
- result = await result();
8
- return result;
9
- };
10
-
11
- const parseChunk = (text) => {
12
- if (!text || !text.startsWith("data:"))
13
- return [void 0, false];
14
- const content = text.slice("data:".length);
15
- const data = content.startsWith(" ") ? content.slice(1) : content;
16
- if (data === "[DONE]") {
17
- return [void 0, true];
18
- }
19
- if (data.startsWith("{") && data.includes('"error":')) {
20
- throw new Error(`Error from server: ${data}`);
21
- }
22
- const chunk = JSON.parse(data);
23
- return [chunk, false];
24
- };
25
- const transformChunk = () => {
26
- const decoder = new TextDecoder();
27
- let buffer = "";
28
- return new TransformStream({
29
- transform: async (chunk, controller) => {
30
- const text = decoder.decode(chunk, { stream: true });
31
- buffer += text;
32
- const lines = buffer.split("\n");
33
- buffer = lines.pop() ?? "";
34
- for (const line of lines) {
35
- try {
36
- const [chunk2, isEnd] = parseChunk(line);
37
- if (isEnd)
38
- break;
39
- if (chunk2) {
40
- controller.enqueue(chunk2);
41
- }
42
- } catch (error) {
43
- controller.error(error);
44
- }
45
- }
46
- }
47
- });
48
- };
49
-
50
- class DelayedPromise {
51
- get promise() {
52
- if (this._promise == null) {
53
- this._promise = new Promise((resolve, reject) => {
54
- if (this.status.type === "resolved") {
55
- resolve(this.status.value);
56
- } else if (this.status.type === "rejected") {
57
- reject(this.status.error);
58
- }
59
- this._resolve = resolve;
60
- this._reject = reject;
61
- });
62
- }
63
- return this._promise;
64
- }
65
- _promise;
66
- _reject;
67
- _resolve;
68
- status = { type: "pending" };
69
- reject(error) {
70
- this.status = { error, type: "rejected" };
71
- if (this._promise) {
72
- this._reject?.(error);
73
- }
74
- }
75
- resolve(value) {
76
- this.status = { type: "resolved", value };
77
- if (this._promise) {
78
- this._resolve?.(value);
79
- }
80
- }
81
- }
82
-
83
- const streamText = async (options) => {
84
- const steps = [];
85
- const messages = structuredClone(options.messages);
86
- const maxSteps = options.maxSteps ?? 1;
87
- let usage;
88
- const resultSteps = new DelayedPromise();
89
- const resultMessages = new DelayedPromise();
90
- const resultUsage = new DelayedPromise();
91
- let eventCtrl;
92
- let textCtrl;
93
- const eventStream = new ReadableStream({ start: (controller) => eventCtrl = controller });
94
- const textStream = new ReadableStream({ start: (controller) => textCtrl = controller });
95
- const pushEvent = (stepEvent) => {
96
- eventCtrl?.enqueue(stepEvent);
97
- void options.onEvent?.(stepEvent);
98
- };
99
- const pushStep = (step) => {
100
- steps.push(step);
101
- void options.onStepFinish?.(step);
102
- };
103
- const startStream = async () => {
104
- const pushUsage = (u) => {
105
- usage = u;
106
- };
107
- let text = "";
108
- const pushText = (content) => {
109
- textCtrl?.enqueue(content);
110
- text += content;
111
- };
112
- const tool_calls = [];
113
- const toolCalls = [];
114
- const toolResults = [];
115
- let finishReason = "other";
116
- await chat({
117
- ...options,
118
- maxSteps: void 0,
119
- messages,
120
- stream: true,
121
- streamOptions: options.streamOptions != null ? objCamelToSnake(options.streamOptions) : void 0
122
- }).then(
123
- async (res) => res.body.pipeThrough(transformChunk()).pipeTo(new WritableStream({
124
- abort: (reason) => {
125
- eventCtrl?.error(reason);
126
- textCtrl?.error(reason);
127
- },
128
- close: () => {
129
- },
130
- write: (chunk) => {
131
- if (chunk.usage)
132
- pushUsage(chunk.usage);
133
- if (chunk.choices == null || chunk.choices.length === 0)
134
- return;
135
- const choice = chunk.choices[0];
136
- if (choice.finish_reason != null)
137
- finishReason = choice.finish_reason;
138
- if (choice.delta.tool_calls?.length === 0 || choice.delta.tool_calls == null) {
139
- if (choice.delta.content != null) {
140
- pushEvent({ text: choice.delta.content, type: "text-delta" });
141
- pushText(choice.delta.content);
142
- } else if (choice.delta.refusal != null) {
143
- pushEvent({ error: choice.delta.refusal, type: "error" });
144
- } else if (choice.finish_reason != null) {
145
- pushEvent({ finishReason: choice.finish_reason, type: "finish", usage });
146
- }
147
- } else {
148
- for (const toolCall of choice.delta.tool_calls) {
149
- const { index } = toolCall;
150
- if (!tool_calls.at(index)) {
151
- tool_calls[index] = toolCall;
152
- pushEvent({ toolCallId: toolCall.id, toolName: toolCall.function.name, type: "tool-call-streaming-start" });
153
- } else {
154
- tool_calls[index].function.arguments += toolCall.function.arguments;
155
- pushEvent({ argsTextDelta: toolCall.function.arguments, toolCallId: toolCall.id, toolName: toolCall.function.name, type: "tool-call-delta" });
156
- }
157
- }
158
- }
159
- }
160
- }))
161
- );
162
- if (tool_calls.length !== 0) {
163
- for (const toolCall of tool_calls) {
164
- const { completionToolCall, completionToolResult, message } = await executeTool({
165
- abortSignal: options.abortSignal,
166
- messages,
167
- toolCall,
168
- tools: options.tools
169
- });
170
- pushEvent({ ...completionToolCall, type: "tool-call" });
171
- pushEvent({ ...completionToolResult, type: "tool-result" });
172
- toolCalls.push(completionToolCall);
173
- toolResults.push(completionToolResult);
174
- messages.push(message);
175
- }
176
- } else {
177
- messages.push({ content: text, role: "assistant" });
178
- }
179
- pushStep({
180
- finishReason,
181
- stepType: determineStepType({ finishReason, maxSteps, stepsLength: steps.length, toolCallsLength: toolCalls.length }),
182
- text,
183
- toolCalls,
184
- toolResults,
185
- usage
186
- });
187
- if (toolCalls.length !== 0 && steps.length < maxSteps)
188
- return async () => startStream();
189
- };
190
- try {
191
- await trampoline(async () => startStream());
192
- } catch (err) {
193
- eventCtrl?.error(err);
194
- textCtrl?.error(err);
195
- resultSteps.reject(err);
196
- resultMessages.reject(err);
197
- resultUsage.reject(err);
198
- } finally {
199
- eventCtrl?.close();
200
- textCtrl?.close();
201
- resultSteps.resolve(steps);
202
- resultMessages.resolve(messages);
203
- resultUsage.resolve(usage);
204
- void options.onFinish?.(steps.at(-1));
205
- }
206
- return {
207
- fullStream: eventStream,
208
- messages: resultMessages.promise,
209
- steps: resultSteps.promise,
210
- textStream,
211
- usage: resultUsage.promise
212
- };
213
- };
214
-
215
- export { streamText };