@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 +61 -9
- package/dist/index.js +249 -48
- package/package.json +10 -9
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
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
});
|
|
41
|
+
const stepStream = new ReadableStream({
|
|
42
|
+
start(controller) {
|
|
43
|
+
stepCtrl = controller;
|
|
23
44
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
"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/
|
|
37
|
-
"@xsai/
|
|
35
|
+
"@xsai/shared": "",
|
|
36
|
+
"@xsai/tool": "",
|
|
37
|
+
"valibot": "^1.0.0-rc.1"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"build": "
|
|
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
|
}
|