@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 +22 -94
- package/dist/index.js +175 -235
- package/package.json +4 -8
- package/dist/case-PdS00lUs.js +0 -4
- package/dist/experimental/index.d.ts +0 -55
- package/dist/experimental/index.js +0 -215
package/dist/index.d.ts
CHANGED
|
@@ -1,85 +1,40 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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(
|
|
47
|
+
if (!text || !text.startsWith("data:"))
|
|
16
48
|
return [void 0, false];
|
|
17
|
-
const content = text.slice(
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
...
|
|
119
|
+
...options,
|
|
68
120
|
maxSteps: void 0,
|
|
121
|
+
messages,
|
|
69
122
|
stream: true,
|
|
70
|
-
streamOptions:
|
|
123
|
+
streamOptions: options.streamOptions != null ? objCamelToSnake(options.streamOptions) : void 0
|
|
71
124
|
}).then(
|
|
72
|
-
async (res) => res.body.pipeThrough(new
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
171
|
+
tools: options.tools
|
|
225
172
|
});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
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
|
|
32
|
+
"@xsai/shared-chat": "~0.3.0"
|
|
37
33
|
},
|
|
38
34
|
"devDependencies": {
|
|
39
35
|
"valibot": "^1.0.0",
|
|
40
|
-
"@xsai/shared": "~0.3.0
|
|
41
|
-
"@xsai/tool": "~0.3.0
|
|
36
|
+
"@xsai/shared": "~0.3.0",
|
|
37
|
+
"@xsai/tool": "~0.3.0"
|
|
42
38
|
},
|
|
43
39
|
"scripts": {
|
|
44
40
|
"build": "pkgroll",
|
package/dist/case-PdS00lUs.js
DELETED
|
@@ -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 };
|