@xsai/stream-text 0.3.5 → 0.4.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ https://xsai.js.org/docs/packages/stream/text
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { WithUnknown } from '@xsai/shared';
1
2
  import { CompletionToolCall, CompletionToolResult, FinishReason, Usage, ChatOptions, CompletionStep, Message } from '@xsai/shared-chat';
2
3
 
3
4
  type StreamTextEvent = (CompletionToolCall & {
@@ -16,6 +17,9 @@ type StreamTextEvent = (CompletionToolCall & {
16
17
  finishReason: FinishReason;
17
18
  type: 'finish';
18
19
  usage?: Usage;
20
+ } | {
21
+ text: string;
22
+ type: 'reasoning-delta';
19
23
  } | {
20
24
  text: string;
21
25
  type: 'text-delta';
@@ -48,8 +52,10 @@ interface StreamTextResult {
48
52
  messages: Promise<Message[]>;
49
53
  steps: Promise<CompletionStep[]>;
50
54
  textStream: ReadableStream<string>;
55
+ totalUsage: Promise<undefined | Usage>;
51
56
  usage: Promise<undefined | Usage>;
52
57
  }
53
- declare const streamText: (options: StreamTextOptions) => Promise<StreamTextResult>;
58
+ declare const streamText: (options: WithUnknown<StreamTextOptions>) => StreamTextResult;
54
59
 
55
- export { type StreamTextEvent, type StreamTextOptions, type StreamTextResult, streamText };
60
+ export { streamText };
61
+ export type { StreamTextEvent, StreamTextOptions, StreamTextResult };
package/dist/index.js CHANGED
@@ -1,48 +1,6 @@
1
+ import { DelayedPromise, trampoline, objCamelToSnake } from '@xsai/shared';
1
2
  import { chat, executeTool, determineStepType } from '@xsai/shared-chat';
2
3
 
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
- }
43
- }
44
- }
45
-
46
4
  const parseChunk = (text) => {
47
5
  if (!text || !text.startsWith("data:"))
48
6
  return [void 0, false];
@@ -82,14 +40,16 @@ const transformChunk = () => {
82
40
  });
83
41
  };
84
42
 
85
- const streamText = async (options) => {
43
+ const streamText = (options) => {
86
44
  const steps = [];
87
45
  const messages = structuredClone(options.messages);
88
46
  const maxSteps = options.maxSteps ?? 1;
89
47
  let usage;
48
+ let totalUsage;
90
49
  const resultSteps = new DelayedPromise();
91
50
  const resultMessages = new DelayedPromise();
92
51
  const resultUsage = new DelayedPromise();
52
+ const resultTotalUsage = new DelayedPromise();
93
53
  let eventCtrl;
94
54
  let textCtrl;
95
55
  const eventStream = new ReadableStream({ start: (controller) => eventCtrl = controller });
@@ -102,9 +62,21 @@ const streamText = async (options) => {
102
62
  steps.push(step);
103
63
  void options.onStepFinish?.(step);
104
64
  };
105
- const startStream = async () => {
65
+ const doStream = async () => {
66
+ const { body: stream } = await chat({
67
+ ...options,
68
+ maxSteps: void 0,
69
+ messages,
70
+ stream: true,
71
+ streamOptions: options.streamOptions != null ? objCamelToSnake(options.streamOptions) : void 0
72
+ });
106
73
  const pushUsage = (u) => {
107
74
  usage = u;
75
+ totalUsage = totalUsage ? {
76
+ completion_tokens: totalUsage.completion_tokens + u.completion_tokens,
77
+ prompt_tokens: totalUsage.prompt_tokens + u.prompt_tokens,
78
+ total_tokens: totalUsage.total_tokens + u.total_tokens
79
+ } : { ...u };
108
80
  };
109
81
  let text = "";
110
82
  const pushText = (content) => {
@@ -115,55 +87,58 @@ const streamText = async (options) => {
115
87
  const toolCalls = [];
116
88
  const toolResults = [];
117
89
  let finishReason = "other";
118
- await chat({
119
- ...options,
120
- maxSteps: void 0,
121
- messages,
122
- stream: true,
123
- streamOptions: options.streamOptions != null ? objCamelToSnake(options.streamOptions) : void 0
124
- }).then(
125
- async (res) => res.body.pipeThrough(transformChunk()).pipeTo(new WritableStream({
126
- abort: (reason) => {
127
- eventCtrl?.error(reason);
128
- textCtrl?.error(reason);
129
- },
130
- close: () => {
131
- },
132
- write: (chunk) => {
133
- if (chunk.usage)
134
- pushUsage(chunk.usage);
135
- if (chunk.choices == null || chunk.choices.length === 0)
136
- return;
137
- const choice = chunk.choices[0];
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 });
148
- }
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
- }
90
+ await stream.pipeThrough(transformChunk()).pipeTo(new WritableStream({
91
+ abort: (reason) => {
92
+ eventCtrl?.error(reason);
93
+ textCtrl?.error(reason);
94
+ },
95
+ close: () => {
96
+ },
97
+ // eslint-disable-next-line sonarjs/cognitive-complexity
98
+ write: (chunk) => {
99
+ if (chunk.usage)
100
+ pushUsage(chunk.usage);
101
+ if (chunk.choices == null || chunk.choices.length === 0)
102
+ return;
103
+ const choice = chunk.choices[0];
104
+ if (choice.delta.reasoning_content != null)
105
+ pushEvent({ text: choice.delta.reasoning_content, type: "reasoning-delta" });
106
+ if (choice.finish_reason != null)
107
+ finishReason = choice.finish_reason;
108
+ if (choice.delta.tool_calls?.length === 0 || choice.delta.tool_calls == null) {
109
+ if (choice.delta.content != null) {
110
+ pushEvent({ text: choice.delta.content, type: "text-delta" });
111
+ pushText(choice.delta.content);
112
+ } else if (choice.delta.refusal != null) {
113
+ pushEvent({ error: choice.delta.refusal, type: "error" });
114
+ } else if (choice.finish_reason != null) {
115
+ pushEvent({ finishReason: choice.finish_reason, type: "finish", usage });
116
+ }
117
+ } else {
118
+ for (const toolCall of choice.delta.tool_calls) {
119
+ const { index } = toolCall;
120
+ if (!tool_calls.at(index)) {
121
+ tool_calls[index] = {
122
+ ...toolCall,
123
+ function: {
124
+ ...toolCall.function,
125
+ arguments: toolCall.function.arguments ?? ""
126
+ }
127
+ };
128
+ pushEvent({ toolCallId: toolCall.id, toolName: toolCall.function.name, type: "tool-call-streaming-start" });
129
+ } else {
130
+ tool_calls[index].function.arguments += toolCall.function.arguments;
131
+ pushEvent({ argsTextDelta: toolCall.function.arguments, toolCallId: toolCall.id, toolName: toolCall.function.name ?? tool_calls[index].function.name, type: "tool-call-delta" });
159
132
  }
160
133
  }
161
134
  }
162
- }))
163
- );
135
+ }
136
+ }));
164
137
  messages.push({ content: text, role: "assistant", tool_calls });
165
138
  if (tool_calls.length !== 0) {
166
139
  for (const toolCall of tool_calls) {
140
+ if (toolCall == null)
141
+ continue;
167
142
  const { completionToolCall, completionToolResult, message } = await executeTool({
168
143
  abortSignal: options.abortSignal,
169
144
  messages,
@@ -192,29 +167,34 @@ const streamText = async (options) => {
192
167
  usage
193
168
  });
194
169
  if (toolCalls.length !== 0 && steps.length < maxSteps)
195
- return async () => startStream();
170
+ return async () => doStream();
196
171
  };
197
- try {
198
- await trampoline(async () => startStream());
199
- eventCtrl?.close();
200
- textCtrl?.close();
201
- } catch (err) {
202
- eventCtrl?.error(err);
203
- textCtrl?.error(err);
204
- resultSteps.reject(err);
205
- resultMessages.reject(err);
206
- resultUsage.reject(err);
207
- } finally {
208
- resultSteps.resolve(steps);
209
- resultMessages.resolve(messages);
210
- resultUsage.resolve(usage);
211
- void options.onFinish?.(steps.at(-1));
212
- }
172
+ void (async () => {
173
+ try {
174
+ await trampoline(async () => doStream());
175
+ eventCtrl?.close();
176
+ textCtrl?.close();
177
+ } catch (err) {
178
+ eventCtrl?.error(err);
179
+ textCtrl?.error(err);
180
+ resultSteps.reject(err);
181
+ resultMessages.reject(err);
182
+ resultUsage.reject(err);
183
+ resultTotalUsage.reject(err);
184
+ } finally {
185
+ resultSteps.resolve(steps);
186
+ resultMessages.resolve(messages);
187
+ resultUsage.resolve(usage);
188
+ resultTotalUsage.resolve(totalUsage);
189
+ void options.onFinish?.(steps.at(-1));
190
+ }
191
+ })();
213
192
  return {
214
193
  fullStream: eventStream,
215
194
  messages: resultMessages.promise,
216
195
  steps: resultSteps.promise,
217
196
  textStream,
197
+ totalUsage: resultTotalUsage.promise,
218
198
  usage: resultUsage.promise
219
199
  };
220
200
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xsai/stream-text",
3
3
  "type": "module",
4
- "version": "0.3.5",
4
+ "version": "0.4.0-beta.10",
5
5
  "description": "extra-small AI SDK.",
6
6
  "author": "Moeru AI",
7
7
  "license": "MIT",
@@ -29,12 +29,12 @@
29
29
  "dist"
30
30
  ],
31
31
  "dependencies": {
32
- "@xsai/shared-chat": "~0.3.5"
32
+ "@xsai/shared": "~0.4.0-beta.10",
33
+ "@xsai/shared-chat": "~0.4.0-beta.10"
33
34
  },
34
35
  "devDependencies": {
35
36
  "valibot": "^1.0.0",
36
- "@xsai/shared": "~0.3.5",
37
- "@xsai/tool": "~0.3.5"
37
+ "@xsai/tool": "~0.4.0-beta.10"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "pkgroll",