assistant-stream 0.2.45 → 0.2.47
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/core/AssistantStream.d.ts +1 -1
- package/dist/core/AssistantStream.d.ts.map +1 -1
- package/dist/core/AssistantStream.js +15 -19
- package/dist/core/AssistantStream.js.map +1 -1
- package/dist/core/AssistantStreamChunk.d.ts +2 -2
- package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
- package/dist/core/AssistantStreamChunk.js +1 -0
- package/dist/core/AssistantStreamChunk.js.map +1 -1
- package/dist/core/accumulators/AssistantMessageStream.d.ts +2 -2
- package/dist/core/accumulators/AssistantMessageStream.d.ts.map +1 -1
- package/dist/core/accumulators/AssistantMessageStream.js +45 -50
- package/dist/core/accumulators/AssistantMessageStream.js.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.d.ts +3 -3
- package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.js +339 -329
- package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
- package/dist/core/index.d.ts +17 -16
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +10 -44
- package/dist/core/index.js.map +1 -1
- package/dist/core/modules/assistant-stream.d.ts +7 -7
- package/dist/core/modules/assistant-stream.d.ts.map +1 -1
- package/dist/core/modules/assistant-stream.js +159 -188
- package/dist/core/modules/assistant-stream.js.map +1 -1
- package/dist/core/modules/text.d.ts +2 -2
- package/dist/core/modules/text.d.ts.map +1 -1
- package/dist/core/modules/text.js +43 -47
- package/dist/core/modules/text.js.map +1 -1
- package/dist/core/modules/tool-call.d.ts +5 -5
- package/dist/core/modules/tool-call.d.ts.map +1 -1
- package/dist/core/modules/tool-call.js +88 -89
- package/dist/core/modules/tool-call.js.map +1 -1
- package/dist/core/object/ObjectStreamAccumulator.d.ts +2 -2
- package/dist/core/object/ObjectStreamAccumulator.d.ts.map +1 -1
- package/dist/core/object/ObjectStreamAccumulator.js +49 -58
- package/dist/core/object/ObjectStreamAccumulator.js.map +1 -1
- package/dist/core/object/ObjectStreamResponse.d.ts +2 -2
- package/dist/core/object/ObjectStreamResponse.d.ts.map +1 -1
- package/dist/core/object/ObjectStreamResponse.js +70 -74
- package/dist/core/object/ObjectStreamResponse.js.map +1 -1
- package/dist/core/object/createObjectStream.d.ts +2 -2
- package/dist/core/object/createObjectStream.d.ts.map +1 -1
- package/dist/core/object/createObjectStream.js +45 -56
- package/dist/core/object/createObjectStream.js.map +1 -1
- package/dist/core/object/types.d.ts +1 -1
- package/dist/core/object/types.d.ts.map +1 -1
- package/dist/core/object/types.js +1 -0
- package/dist/core/object/types.js.map +1 -1
- package/dist/core/serialization/PlainText.d.ts +3 -3
- package/dist/core/serialization/PlainText.d.ts.map +1 -1
- package/dist/core/serialization/PlainText.js +46 -47
- package/dist/core/serialization/PlainText.js.map +1 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts +3 -3
- package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts.map +1 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.js +117 -112
- package/dist/core/serialization/assistant-transport/AssistantTransport.js.map +1 -1
- package/dist/core/serialization/data-stream/DataStream.d.ts +3 -3
- package/dist/core/serialization/data-stream/DataStream.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/DataStream.js +355 -354
- package/dist/core/serialization/data-stream/DataStream.js.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.d.ts +2 -2
- package/dist/core/serialization/data-stream/chunk-types.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.js +22 -26
- package/dist/core/serialization/data-stream/chunk-types.js.map +1 -1
- package/dist/core/serialization/data-stream/serialization.d.ts +1 -1
- package/dist/core/serialization/data-stream/serialization.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/serialization.js +23 -28
- package/dist/core/serialization/data-stream/serialization.js.map +1 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts +19 -0
- package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts.map +1 -0
- package/dist/core/serialization/ui-message-stream/UIMessageStream.js +231 -0
- package/dist/core/serialization/ui-message-stream/UIMessageStream.js.map +1 -0
- package/dist/core/serialization/ui-message-stream/chunk-types.d.ts +78 -0
- package/dist/core/serialization/ui-message-stream/chunk-types.d.ts.map +1 -0
- package/dist/core/serialization/ui-message-stream/chunk-types.js +2 -0
- package/dist/core/serialization/ui-message-stream/chunk-types.js.map +1 -0
- package/dist/core/tool/ToolCallReader.d.ts +4 -4
- package/dist/core/tool/ToolCallReader.d.ts.map +1 -1
- package/dist/core/tool/ToolCallReader.js +303 -303
- package/dist/core/tool/ToolCallReader.js.map +1 -1
- package/dist/core/tool/ToolExecutionStream.d.ts +5 -5
- package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
- package/dist/core/tool/ToolExecutionStream.js +140 -143
- package/dist/core/tool/ToolExecutionStream.js.map +1 -1
- package/dist/core/tool/ToolResponse.d.ts +1 -1
- package/dist/core/tool/ToolResponse.d.ts.map +1 -1
- package/dist/core/tool/ToolResponse.js +25 -29
- package/dist/core/tool/ToolResponse.js.map +1 -1
- package/dist/core/tool/index.d.ts +5 -5
- package/dist/core/tool/index.d.ts.map +1 -1
- package/dist/core/tool/index.js +3 -13
- package/dist/core/tool/index.js.map +1 -1
- package/dist/core/tool/tool-types.d.ts +3 -3
- package/dist/core/tool/tool-types.d.ts.map +1 -1
- package/dist/core/tool/tool-types.js +1 -0
- package/dist/core/tool/tool-types.js.map +1 -1
- package/dist/core/tool/toolResultStream.d.ts +3 -3
- package/dist/core/tool/toolResultStream.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.js +118 -125
- package/dist/core/tool/toolResultStream.js.map +1 -1
- package/dist/core/tool/type-path-utils.js +1 -0
- package/dist/core/tool/type-path-utils.js.map +1 -1
- package/dist/core/utils/Counter.js +6 -10
- package/dist/core/utils/Counter.js.map +1 -1
- package/dist/core/utils/generateId.js +1 -8
- package/dist/core/utils/generateId.js.map +1 -1
- package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts +1 -1
- package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/AssistantMetaTransformStream.js +42 -43
- package/dist/core/utils/stream/AssistantMetaTransformStream.js.map +1 -1
- package/dist/core/utils/stream/AssistantTransformStream.d.ts +2 -2
- package/dist/core/utils/stream/AssistantTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/AssistantTransformStream.js +35 -45
- package/dist/core/utils/stream/AssistantTransformStream.js.map +1 -1
- package/dist/core/utils/stream/LineDecoderStream.js +24 -26
- package/dist/core/utils/stream/LineDecoderStream.js.map +1 -1
- package/dist/core/utils/stream/PipeableTransformStream.js +10 -14
- package/dist/core/utils/stream/PipeableTransformStream.js.map +1 -1
- package/dist/core/utils/stream/SSE.d.ts +1 -1
- package/dist/core/utils/stream/SSE.d.ts.map +1 -1
- package/dist/core/utils/stream/SSE.js +90 -98
- package/dist/core/utils/stream/SSE.js.map +1 -1
- package/dist/core/utils/stream/UnderlyingReadable.js +1 -0
- package/dist/core/utils/stream/UnderlyingReadable.js.map +1 -1
- package/dist/core/utils/stream/merge.d.ts +1 -1
- package/dist/core/utils/stream/merge.d.ts.map +1 -1
- package/dist/core/utils/stream/merge.js +169 -81
- package/dist/core/utils/stream/merge.js.map +1 -1
- package/dist/core/utils/stream/path-utils.d.ts +2 -2
- package/dist/core/utils/stream/path-utils.d.ts.map +1 -1
- package/dist/core/utils/stream/path-utils.js +49 -56
- package/dist/core/utils/stream/path-utils.js.map +1 -1
- package/dist/core/utils/types.d.ts +1 -1
- package/dist/core/utils/types.d.ts.map +1 -1
- package/dist/core/utils/types.js +1 -0
- package/dist/core/utils/types.js.map +1 -1
- package/dist/core/utils/withPromiseOrValue.js +14 -14
- package/dist/core/utils/withPromiseOrValue.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/AsyncIterableStream.js +15 -16
- package/dist/utils/AsyncIterableStream.js.map +1 -1
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -7
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/json/fix-json.js +365 -343
- package/dist/utils/json/fix-json.js.map +1 -1
- package/dist/utils/json/index.d.ts +1 -1
- package/dist/utils/json/index.d.ts.map +1 -1
- package/dist/utils/json/index.js +1 -0
- package/dist/utils/json/index.js.map +1 -1
- package/dist/utils/json/is-json.d.ts +1 -1
- package/dist/utils/json/is-json.d.ts.map +1 -1
- package/dist/utils/json/is-json.js +21 -25
- package/dist/utils/json/is-json.js.map +1 -1
- package/dist/utils/json/json-value.js +1 -0
- package/dist/utils/json/json-value.js.map +1 -1
- package/dist/utils/json/parse-partial-json-object.d.ts +1 -1
- package/dist/utils/json/parse-partial-json-object.d.ts.map +1 -1
- package/dist/utils/json/parse-partial-json-object.js +61 -56
- package/dist/utils/json/parse-partial-json-object.js.map +1 -1
- package/dist/utils/promiseWithResolvers.js +10 -13
- package/dist/utils/promiseWithResolvers.js.map +1 -1
- package/dist/utils.d.ts +5 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +4 -17
- package/dist/utils.js.map +1 -1
- package/package.json +26 -14
- package/src/core/index.ts +6 -0
- package/src/core/serialization/ui-message-stream/UIMessageStream.test.ts +370 -0
- package/src/core/serialization/ui-message-stream/UIMessageStream.ts +300 -0
- package/src/core/serialization/ui-message-stream/chunk-types.ts +60 -0
- package/dist/core/object/ObjectStream.test.d.ts +0 -2
- package/dist/core/object/ObjectStream.test.d.ts.map +0 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.test.d.ts +0 -2
- package/dist/core/serialization/assistant-transport/AssistantTransport.test.d.ts.map +0 -1
- package/dist/core/tool/toolResultStream.test.d.ts +0 -2
- package/dist/core/tool/toolResultStream.test.d.ts.map +0 -1
- package/dist/utils/json/parse-partial-json-object.test.d.ts +0 -2
- package/dist/utils/json/parse-partial-json-object.test.d.ts.map +0 -1
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { UIMessageStreamDecoder } from "./UIMessageStream";
|
|
3
|
+
import type { AssistantStreamChunk } from "../../AssistantStreamChunk";
|
|
4
|
+
|
|
5
|
+
// Helper function to collect all chunks from a stream
|
|
6
|
+
async function collectChunks<T>(stream: ReadableStream<T>): Promise<T[]> {
|
|
7
|
+
const reader = stream.getReader();
|
|
8
|
+
const chunks: T[] = [];
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
while (true) {
|
|
12
|
+
const { done, value } = await reader.read();
|
|
13
|
+
if (done) break;
|
|
14
|
+
chunks.push(value);
|
|
15
|
+
}
|
|
16
|
+
} finally {
|
|
17
|
+
reader.releaseLock();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return chunks;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Helper function to create a UI Message Stream from events
|
|
24
|
+
function createUIMessageStream(events: string[]): ReadableStream<Uint8Array> {
|
|
25
|
+
const encoder = new TextEncoder();
|
|
26
|
+
const sseText = events.map((e) => `data: ${e}\n\n`).join("");
|
|
27
|
+
|
|
28
|
+
return new ReadableStream({
|
|
29
|
+
start(controller) {
|
|
30
|
+
controller.enqueue(encoder.encode(sseText));
|
|
31
|
+
controller.close();
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("UIMessageStreamDecoder", () => {
|
|
37
|
+
it("should decode text deltas", async () => {
|
|
38
|
+
const events = [
|
|
39
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
40
|
+
JSON.stringify({ type: "text-start", id: "text_1" }),
|
|
41
|
+
JSON.stringify({ type: "text-delta", textDelta: "Hello" }),
|
|
42
|
+
JSON.stringify({ type: "text-delta", textDelta: " world" }),
|
|
43
|
+
JSON.stringify({ type: "text-end" }),
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
type: "finish",
|
|
46
|
+
finishReason: "stop",
|
|
47
|
+
usage: { promptTokens: 10, completionTokens: 5 },
|
|
48
|
+
}),
|
|
49
|
+
"[DONE]",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const stream = createUIMessageStream(events);
|
|
53
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
54
|
+
const chunks = await collectChunks(decodedStream);
|
|
55
|
+
|
|
56
|
+
// Find text-delta chunks
|
|
57
|
+
const textDeltas = chunks.filter(
|
|
58
|
+
(c): c is AssistantStreamChunk & { type: "text-delta" } =>
|
|
59
|
+
c.type === "text-delta",
|
|
60
|
+
);
|
|
61
|
+
expect(textDeltas).toHaveLength(2);
|
|
62
|
+
expect(textDeltas[0]?.textDelta).toBe("Hello");
|
|
63
|
+
expect(textDeltas[1]?.textDelta).toBe(" world");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should decode reasoning parts", async () => {
|
|
67
|
+
const events = [
|
|
68
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
69
|
+
JSON.stringify({ type: "reasoning-start", id: "reasoning_1" }),
|
|
70
|
+
JSON.stringify({ type: "reasoning-delta", delta: "Let me think..." }),
|
|
71
|
+
JSON.stringify({ type: "reasoning-end" }),
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
type: "finish",
|
|
74
|
+
finishReason: "stop",
|
|
75
|
+
usage: { promptTokens: 10, completionTokens: 5 },
|
|
76
|
+
}),
|
|
77
|
+
"[DONE]",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const stream = createUIMessageStream(events);
|
|
81
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
82
|
+
const chunks = await collectChunks(decodedStream);
|
|
83
|
+
|
|
84
|
+
// Find part-start for reasoning
|
|
85
|
+
const partStarts = chunks.filter(
|
|
86
|
+
(c): c is AssistantStreamChunk & { type: "part-start" } =>
|
|
87
|
+
c.type === "part-start",
|
|
88
|
+
);
|
|
89
|
+
expect(partStarts.some((p) => p.part.type === "reasoning")).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should decode tool calls", async () => {
|
|
93
|
+
const events = [
|
|
94
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
95
|
+
JSON.stringify({
|
|
96
|
+
type: "tool-call-start",
|
|
97
|
+
id: "tc_1",
|
|
98
|
+
toolCallId: "call_abc",
|
|
99
|
+
toolName: "weather",
|
|
100
|
+
}),
|
|
101
|
+
JSON.stringify({ type: "tool-call-delta", argsText: '{"city":' }),
|
|
102
|
+
JSON.stringify({ type: "tool-call-delta", argsText: '"NYC"}' }),
|
|
103
|
+
JSON.stringify({ type: "tool-call-end" }),
|
|
104
|
+
JSON.stringify({
|
|
105
|
+
type: "tool-result",
|
|
106
|
+
toolCallId: "call_abc",
|
|
107
|
+
result: { temp: 72 },
|
|
108
|
+
}),
|
|
109
|
+
JSON.stringify({
|
|
110
|
+
type: "finish",
|
|
111
|
+
finishReason: "stop",
|
|
112
|
+
usage: { promptTokens: 10, completionTokens: 5 },
|
|
113
|
+
}),
|
|
114
|
+
"[DONE]",
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const stream = createUIMessageStream(events);
|
|
118
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
119
|
+
const chunks = await collectChunks(decodedStream);
|
|
120
|
+
|
|
121
|
+
// Find tool-call part-start
|
|
122
|
+
const toolCallStart = chunks.find(
|
|
123
|
+
(c): c is AssistantStreamChunk & { type: "part-start" } =>
|
|
124
|
+
c.type === "part-start" && c.part.type === "tool-call",
|
|
125
|
+
);
|
|
126
|
+
expect(toolCallStart).toBeDefined();
|
|
127
|
+
if (toolCallStart?.part.type === "tool-call") {
|
|
128
|
+
expect(toolCallStart.part.toolName).toBe("weather");
|
|
129
|
+
expect(toolCallStart.part.toolCallId).toBe("call_abc");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Find result
|
|
133
|
+
const result = chunks.find(
|
|
134
|
+
(c): c is AssistantStreamChunk & { type: "result" } =>
|
|
135
|
+
c.type === "result",
|
|
136
|
+
);
|
|
137
|
+
expect(result).toBeDefined();
|
|
138
|
+
expect(result?.result).toEqual({ temp: 72 });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should decode source parts", async () => {
|
|
142
|
+
const events = [
|
|
143
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
type: "source",
|
|
146
|
+
source: {
|
|
147
|
+
sourceType: "url",
|
|
148
|
+
id: "src_1",
|
|
149
|
+
url: "https://example.com",
|
|
150
|
+
title: "Example",
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
type: "finish",
|
|
155
|
+
finishReason: "stop",
|
|
156
|
+
usage: { promptTokens: 10, completionTokens: 5 },
|
|
157
|
+
}),
|
|
158
|
+
"[DONE]",
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const stream = createUIMessageStream(events);
|
|
162
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
163
|
+
const chunks = await collectChunks(decodedStream);
|
|
164
|
+
|
|
165
|
+
const sourceStart = chunks.find(
|
|
166
|
+
(c): c is AssistantStreamChunk & { type: "part-start" } =>
|
|
167
|
+
c.type === "part-start" && c.part.type === "source",
|
|
168
|
+
);
|
|
169
|
+
expect(sourceStart).toBeDefined();
|
|
170
|
+
if (sourceStart?.part.type === "source") {
|
|
171
|
+
expect(sourceStart.part.url).toBe("https://example.com");
|
|
172
|
+
expect(sourceStart.part.title).toBe("Example");
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should decode file parts", async () => {
|
|
177
|
+
const events = [
|
|
178
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
179
|
+
JSON.stringify({
|
|
180
|
+
type: "file",
|
|
181
|
+
file: {
|
|
182
|
+
mimeType: "image/png",
|
|
183
|
+
data: "base64data...",
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
JSON.stringify({
|
|
187
|
+
type: "finish",
|
|
188
|
+
finishReason: "stop",
|
|
189
|
+
usage: { promptTokens: 10, completionTokens: 5 },
|
|
190
|
+
}),
|
|
191
|
+
"[DONE]",
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const stream = createUIMessageStream(events);
|
|
195
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
196
|
+
const chunks = await collectChunks(decodedStream);
|
|
197
|
+
|
|
198
|
+
const fileStart = chunks.find(
|
|
199
|
+
(c): c is AssistantStreamChunk & { type: "part-start" } =>
|
|
200
|
+
c.type === "part-start" && c.part.type === "file",
|
|
201
|
+
);
|
|
202
|
+
expect(fileStart).toBeDefined();
|
|
203
|
+
if (fileStart?.part.type === "file") {
|
|
204
|
+
expect(fileStart.part.mimeType).toBe("image/png");
|
|
205
|
+
expect(fileStart.part.data).toBe("base64data...");
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should handle data-* chunks", async () => {
|
|
210
|
+
const onData = vi.fn();
|
|
211
|
+
const events = [
|
|
212
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
213
|
+
JSON.stringify({
|
|
214
|
+
type: "data-weather",
|
|
215
|
+
data: { temp: 72, city: "NYC" },
|
|
216
|
+
}),
|
|
217
|
+
JSON.stringify({
|
|
218
|
+
type: "finish",
|
|
219
|
+
finishReason: "stop",
|
|
220
|
+
usage: { promptTokens: 10, completionTokens: 5 },
|
|
221
|
+
}),
|
|
222
|
+
"[DONE]",
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
const stream = createUIMessageStream(events);
|
|
226
|
+
const decodedStream = stream.pipeThrough(
|
|
227
|
+
new UIMessageStreamDecoder({ onData }),
|
|
228
|
+
);
|
|
229
|
+
await collectChunks(decodedStream);
|
|
230
|
+
|
|
231
|
+
expect(onData).toHaveBeenCalledWith({
|
|
232
|
+
type: "data-weather",
|
|
233
|
+
name: "weather",
|
|
234
|
+
data: { temp: 72, city: "NYC" },
|
|
235
|
+
transient: undefined,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should handle transient data-* chunks", async () => {
|
|
240
|
+
const onData = vi.fn();
|
|
241
|
+
const events = [
|
|
242
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
243
|
+
JSON.stringify({
|
|
244
|
+
type: "data-progress",
|
|
245
|
+
transient: true,
|
|
246
|
+
data: { percent: 50 },
|
|
247
|
+
}),
|
|
248
|
+
JSON.stringify({
|
|
249
|
+
type: "finish",
|
|
250
|
+
finishReason: "stop",
|
|
251
|
+
usage: { promptTokens: 10, completionTokens: 5 },
|
|
252
|
+
}),
|
|
253
|
+
"[DONE]",
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
const stream = createUIMessageStream(events);
|
|
257
|
+
const decodedStream = stream.pipeThrough(
|
|
258
|
+
new UIMessageStreamDecoder({ onData }),
|
|
259
|
+
);
|
|
260
|
+
const chunks = await collectChunks(decodedStream);
|
|
261
|
+
|
|
262
|
+
// Transient data should call onData
|
|
263
|
+
expect(onData).toHaveBeenCalledWith({
|
|
264
|
+
type: "data-progress",
|
|
265
|
+
name: "progress",
|
|
266
|
+
data: { percent: 50 },
|
|
267
|
+
transient: true,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Transient data should NOT emit a data chunk
|
|
271
|
+
const dataChunks = chunks.filter((c) => c.type === "data");
|
|
272
|
+
expect(dataChunks).toHaveLength(0);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should handle step lifecycle", async () => {
|
|
276
|
+
const events = [
|
|
277
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
278
|
+
JSON.stringify({ type: "start-step", messageId: "step_1" }),
|
|
279
|
+
JSON.stringify({ type: "text-start", id: "text_1" }),
|
|
280
|
+
JSON.stringify({ type: "text-delta", textDelta: "Hello" }),
|
|
281
|
+
JSON.stringify({ type: "text-end" }),
|
|
282
|
+
JSON.stringify({
|
|
283
|
+
type: "finish-step",
|
|
284
|
+
finishReason: "stop",
|
|
285
|
+
usage: { promptTokens: 10, completionTokens: 5 },
|
|
286
|
+
isContinued: false,
|
|
287
|
+
}),
|
|
288
|
+
JSON.stringify({
|
|
289
|
+
type: "finish",
|
|
290
|
+
finishReason: "stop",
|
|
291
|
+
usage: { promptTokens: 10, completionTokens: 5 },
|
|
292
|
+
}),
|
|
293
|
+
"[DONE]",
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
const stream = createUIMessageStream(events);
|
|
297
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
298
|
+
const chunks = await collectChunks(decodedStream);
|
|
299
|
+
|
|
300
|
+
// Find step-start chunks
|
|
301
|
+
const stepStarts = chunks.filter((c) => c.type === "step-start");
|
|
302
|
+
expect(stepStarts.length).toBeGreaterThanOrEqual(1);
|
|
303
|
+
|
|
304
|
+
// Find step-finish
|
|
305
|
+
const stepFinish = chunks.find(
|
|
306
|
+
(c): c is AssistantStreamChunk & { type: "step-finish" } =>
|
|
307
|
+
c.type === "step-finish",
|
|
308
|
+
);
|
|
309
|
+
expect(stepFinish).toBeDefined();
|
|
310
|
+
expect(stepFinish?.finishReason).toBe("stop");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("should handle errors", async () => {
|
|
314
|
+
const events = [
|
|
315
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
316
|
+
JSON.stringify({ type: "error", errorText: "Something went wrong" }),
|
|
317
|
+
"[DONE]",
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
const stream = createUIMessageStream(events);
|
|
321
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
322
|
+
const chunks = await collectChunks(decodedStream);
|
|
323
|
+
|
|
324
|
+
const errorChunk = chunks.find(
|
|
325
|
+
(c): c is AssistantStreamChunk & { type: "error" } => c.type === "error",
|
|
326
|
+
);
|
|
327
|
+
expect(errorChunk).toBeDefined();
|
|
328
|
+
expect(errorChunk?.error).toBe("Something went wrong");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("should throw when stream ends without [DONE]", async () => {
|
|
332
|
+
const encoder = new TextEncoder();
|
|
333
|
+
const sseText =
|
|
334
|
+
'data: {"type":"text-delta","textDelta":"Hello"}\n\n' +
|
|
335
|
+
'data: {"type":"text-delta","textDelta":" world"}\n\n';
|
|
336
|
+
|
|
337
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
338
|
+
start(controller) {
|
|
339
|
+
controller.enqueue(encoder.encode(sseText));
|
|
340
|
+
controller.close();
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
345
|
+
|
|
346
|
+
await expect(collectChunks(decodedStream)).rejects.toThrow(
|
|
347
|
+
"Stream ended abruptly without receiving [DONE] marker",
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("should ignore unknown chunk types for forward compatibility", async () => {
|
|
352
|
+
const events = [
|
|
353
|
+
JSON.stringify({ type: "start", messageId: "msg_123" }),
|
|
354
|
+
JSON.stringify({ type: "unknown-future-type", data: {} }),
|
|
355
|
+
JSON.stringify({
|
|
356
|
+
type: "finish",
|
|
357
|
+
finishReason: "stop",
|
|
358
|
+
usage: { promptTokens: 10, completionTokens: 5 },
|
|
359
|
+
}),
|
|
360
|
+
"[DONE]",
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
const stream = createUIMessageStream(events);
|
|
364
|
+
const decodedStream = stream.pipeThrough(new UIMessageStreamDecoder());
|
|
365
|
+
const chunks = await collectChunks(decodedStream);
|
|
366
|
+
|
|
367
|
+
// Should not throw, should complete successfully
|
|
368
|
+
expect(chunks.some((c) => c.type === "message-finish")).toBe(true);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type { AssistantStreamChunk } from "../../AssistantStreamChunk";
|
|
2
|
+
import type { ToolCallStreamController } from "../../modules/tool-call";
|
|
3
|
+
import type { TextStreamController } from "../../modules/text";
|
|
4
|
+
import { AssistantTransformStream } from "../../utils/stream/AssistantTransformStream";
|
|
5
|
+
import { PipeableTransformStream } from "../../utils/stream/PipeableTransformStream";
|
|
6
|
+
import { LineDecoderStream } from "../../utils/stream/LineDecoderStream";
|
|
7
|
+
import {
|
|
8
|
+
type UIMessageStreamChunk,
|
|
9
|
+
type UIMessageStreamDataChunk,
|
|
10
|
+
} from "./chunk-types";
|
|
11
|
+
import { generateId } from "../../utils/generateId";
|
|
12
|
+
|
|
13
|
+
export type { UIMessageStreamChunk, UIMessageStreamDataChunk };
|
|
14
|
+
|
|
15
|
+
export type UIMessageStreamDecoderOptions = {
|
|
16
|
+
onData?: (data: {
|
|
17
|
+
type: string;
|
|
18
|
+
name: string;
|
|
19
|
+
data: unknown;
|
|
20
|
+
transient?: boolean;
|
|
21
|
+
}) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type SSEEvent = {
|
|
25
|
+
event: string;
|
|
26
|
+
data: string;
|
|
27
|
+
id?: string | undefined;
|
|
28
|
+
retry?: number | undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
class SSEEventStream extends TransformStream<string, SSEEvent> {
|
|
32
|
+
constructor() {
|
|
33
|
+
let eventBuffer: Partial<SSEEvent> = {};
|
|
34
|
+
let dataLines: string[] = [];
|
|
35
|
+
|
|
36
|
+
super({
|
|
37
|
+
start() {
|
|
38
|
+
eventBuffer = {};
|
|
39
|
+
dataLines = [];
|
|
40
|
+
},
|
|
41
|
+
transform(line, controller) {
|
|
42
|
+
if (line.startsWith(":")) return;
|
|
43
|
+
|
|
44
|
+
if (line === "") {
|
|
45
|
+
if (dataLines.length > 0) {
|
|
46
|
+
controller.enqueue({
|
|
47
|
+
event: eventBuffer.event || "message",
|
|
48
|
+
data: dataLines.join("\n"),
|
|
49
|
+
id: eventBuffer.id,
|
|
50
|
+
retry: eventBuffer.retry,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
eventBuffer = {};
|
|
54
|
+
dataLines = [];
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const [field, ...rest] = line.split(":");
|
|
59
|
+
const value = rest.join(":").trimStart();
|
|
60
|
+
|
|
61
|
+
switch (field) {
|
|
62
|
+
case "event":
|
|
63
|
+
eventBuffer.event = value;
|
|
64
|
+
break;
|
|
65
|
+
case "data":
|
|
66
|
+
dataLines.push(value);
|
|
67
|
+
break;
|
|
68
|
+
case "id":
|
|
69
|
+
eventBuffer.id = value;
|
|
70
|
+
break;
|
|
71
|
+
case "retry":
|
|
72
|
+
eventBuffer.retry = Number(value);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
flush(controller) {
|
|
77
|
+
if (dataLines.length > 0) {
|
|
78
|
+
controller.enqueue({
|
|
79
|
+
event: eventBuffer.event || "message",
|
|
80
|
+
data: dataLines.join("\n"),
|
|
81
|
+
id: eventBuffer.id,
|
|
82
|
+
retry: eventBuffer.retry,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isDataChunk = (
|
|
91
|
+
chunk: UIMessageStreamChunk,
|
|
92
|
+
): chunk is UIMessageStreamDataChunk => chunk.type.startsWith("data-");
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Decodes AI SDK v6 UI Message Stream format into AssistantStreamChunks.
|
|
96
|
+
*/
|
|
97
|
+
export class UIMessageStreamDecoder extends PipeableTransformStream<
|
|
98
|
+
Uint8Array<ArrayBuffer>,
|
|
99
|
+
AssistantStreamChunk
|
|
100
|
+
> {
|
|
101
|
+
constructor(options: UIMessageStreamDecoderOptions = {}) {
|
|
102
|
+
super((readable) => {
|
|
103
|
+
const toolCallControllers = new Map<string, ToolCallStreamController>();
|
|
104
|
+
let activeToolCallArgsText: TextStreamController | undefined;
|
|
105
|
+
let currentMessageId: string | undefined;
|
|
106
|
+
let receivedDone = false;
|
|
107
|
+
|
|
108
|
+
const transform = new AssistantTransformStream<UIMessageStreamChunk>({
|
|
109
|
+
transform(chunk, controller) {
|
|
110
|
+
const type = chunk.type;
|
|
111
|
+
|
|
112
|
+
if (isDataChunk(chunk)) {
|
|
113
|
+
const name = chunk.type.slice(5);
|
|
114
|
+
|
|
115
|
+
if (options.onData) {
|
|
116
|
+
options.onData({
|
|
117
|
+
type: chunk.type,
|
|
118
|
+
name,
|
|
119
|
+
data: chunk.data,
|
|
120
|
+
...(chunk.transient !== undefined && {
|
|
121
|
+
transient: chunk.transient,
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!chunk.transient) {
|
|
127
|
+
controller.enqueue({
|
|
128
|
+
type: "data",
|
|
129
|
+
path: [],
|
|
130
|
+
data: [{ name, data: chunk.data }],
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
switch (type) {
|
|
137
|
+
case "start":
|
|
138
|
+
currentMessageId = chunk.messageId;
|
|
139
|
+
controller.enqueue({
|
|
140
|
+
type: "step-start",
|
|
141
|
+
path: [],
|
|
142
|
+
messageId: chunk.messageId,
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case "text-start":
|
|
147
|
+
case "text-end":
|
|
148
|
+
case "reasoning-start":
|
|
149
|
+
case "reasoning-end":
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
case "text-delta":
|
|
153
|
+
controller.appendText(chunk.textDelta);
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case "reasoning-delta":
|
|
157
|
+
controller.appendReasoning(chunk.delta);
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case "source":
|
|
161
|
+
controller.appendSource({
|
|
162
|
+
type: "source",
|
|
163
|
+
sourceType: chunk.source.sourceType,
|
|
164
|
+
id: chunk.source.id,
|
|
165
|
+
url: chunk.source.url,
|
|
166
|
+
...(chunk.source.title && { title: chunk.source.title }),
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case "file":
|
|
171
|
+
controller.appendFile({
|
|
172
|
+
type: "file",
|
|
173
|
+
mimeType: chunk.file.mimeType,
|
|
174
|
+
data: chunk.file.data,
|
|
175
|
+
});
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case "tool-call-start": {
|
|
179
|
+
activeToolCallArgsText?.close();
|
|
180
|
+
activeToolCallArgsText = undefined;
|
|
181
|
+
|
|
182
|
+
if (toolCallControllers.has(chunk.toolCallId)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Encountered duplicate tool call id: ${chunk.toolCallId}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const toolCallController = controller.addToolCallPart({
|
|
189
|
+
toolCallId: chunk.toolCallId,
|
|
190
|
+
toolName: chunk.toolName,
|
|
191
|
+
});
|
|
192
|
+
toolCallControllers.set(chunk.toolCallId, toolCallController);
|
|
193
|
+
activeToolCallArgsText = toolCallController.argsText;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case "tool-call-delta":
|
|
198
|
+
activeToolCallArgsText?.append(chunk.argsText);
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case "tool-call-end":
|
|
202
|
+
activeToolCallArgsText?.close();
|
|
203
|
+
activeToolCallArgsText = undefined;
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case "tool-result": {
|
|
207
|
+
const toolCallController = toolCallControllers.get(
|
|
208
|
+
chunk.toolCallId,
|
|
209
|
+
);
|
|
210
|
+
if (!toolCallController) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Encountered tool result with unknown id: ${chunk.toolCallId}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
toolCallController.setResponse({
|
|
216
|
+
result: chunk.result,
|
|
217
|
+
isError: chunk.isError ?? false,
|
|
218
|
+
});
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case "start-step":
|
|
223
|
+
controller.enqueue({
|
|
224
|
+
type: "step-start",
|
|
225
|
+
path: [],
|
|
226
|
+
messageId: chunk.messageId ?? currentMessageId ?? generateId(),
|
|
227
|
+
});
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
case "finish-step":
|
|
231
|
+
controller.enqueue({
|
|
232
|
+
type: "step-finish",
|
|
233
|
+
path: [],
|
|
234
|
+
finishReason: chunk.finishReason,
|
|
235
|
+
usage: chunk.usage,
|
|
236
|
+
isContinued: chunk.isContinued,
|
|
237
|
+
});
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case "finish":
|
|
241
|
+
controller.enqueue({
|
|
242
|
+
type: "message-finish",
|
|
243
|
+
path: [],
|
|
244
|
+
finishReason: chunk.finishReason,
|
|
245
|
+
usage: chunk.usage,
|
|
246
|
+
});
|
|
247
|
+
break;
|
|
248
|
+
|
|
249
|
+
case "error":
|
|
250
|
+
controller.enqueue({
|
|
251
|
+
type: "error",
|
|
252
|
+
path: [],
|
|
253
|
+
error: chunk.errorText,
|
|
254
|
+
});
|
|
255
|
+
break;
|
|
256
|
+
|
|
257
|
+
default:
|
|
258
|
+
// ignore unknown types for forward compatibility
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
flush() {
|
|
263
|
+
activeToolCallArgsText?.close();
|
|
264
|
+
toolCallControllers.forEach((ctrl) => ctrl.close());
|
|
265
|
+
toolCallControllers.clear();
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return readable
|
|
270
|
+
.pipeThrough(new TextDecoderStream())
|
|
271
|
+
.pipeThrough(new LineDecoderStream())
|
|
272
|
+
.pipeThrough(new SSEEventStream())
|
|
273
|
+
.pipeThrough(
|
|
274
|
+
new TransformStream<SSEEvent, UIMessageStreamChunk>({
|
|
275
|
+
transform(event, controller) {
|
|
276
|
+
if (event.event !== "message") {
|
|
277
|
+
throw new Error(`Unknown SSE event type: ${event.event}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (event.data === "[DONE]") {
|
|
281
|
+
receivedDone = true;
|
|
282
|
+
controller.terminate();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
controller.enqueue(JSON.parse(event.data));
|
|
287
|
+
},
|
|
288
|
+
flush() {
|
|
289
|
+
if (!receivedDone) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
"Stream ended abruptly without receiving [DONE] marker",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
)
|
|
297
|
+
.pipeThrough(transform);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|