@copilotkit/runtime 1.55.2-next.0 → 1.55.2-next.1
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/CHANGELOG.md +7 -0
- package/dist/agent/converters/aisdk.cjs +215 -0
- package/dist/agent/converters/aisdk.cjs.map +1 -0
- package/dist/agent/converters/aisdk.d.cts +18 -0
- package/dist/agent/converters/aisdk.d.cts.map +1 -0
- package/dist/agent/converters/aisdk.d.mts +18 -0
- package/dist/agent/converters/aisdk.d.mts.map +1 -0
- package/dist/agent/converters/aisdk.mjs +214 -0
- package/dist/agent/converters/aisdk.mjs.map +1 -0
- package/dist/agent/converters/index.d.mts +3 -0
- package/dist/agent/converters/tanstack.cjs +180 -0
- package/dist/agent/converters/tanstack.cjs.map +1 -0
- package/dist/agent/converters/tanstack.d.cts +68 -0
- package/dist/agent/converters/tanstack.d.cts.map +1 -0
- package/dist/agent/converters/tanstack.d.mts +68 -0
- package/dist/agent/converters/tanstack.d.mts.map +1 -0
- package/dist/agent/converters/tanstack.mjs +178 -0
- package/dist/agent/converters/tanstack.mjs.map +1 -0
- package/dist/agent/index.cjs +111 -17
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +61 -4
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts +62 -4
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +111 -17
- package/dist/agent/index.mjs.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.cjs.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.d.cts.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.d.mts.map +1 -1
- package/dist/lib/integrations/nextjs/pages-router.mjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.cjs +4 -2
- package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.mjs +4 -2
- package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.cjs +1 -1
- package/dist/lib/runtime/mcp-tools-utils.cjs.map +1 -1
- package/dist/lib/runtime/mcp-tools-utils.mjs +1 -1
- package/dist/lib/runtime/mcp-tools-utils.mjs.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/service-adapters/anthropic/utils.cjs +1 -1
- package/dist/service-adapters/anthropic/utils.cjs.map +1 -1
- package/dist/service-adapters/anthropic/utils.mjs +1 -1
- package/dist/service-adapters/anthropic/utils.mjs.map +1 -1
- package/dist/service-adapters/openai/utils.cjs +1 -1
- package/dist/service-adapters/openai/utils.cjs.map +1 -1
- package/dist/service-adapters/openai/utils.mjs +1 -1
- package/dist/service-adapters/openai/utils.mjs.map +1 -1
- package/dist/v2/index.cjs +5 -0
- package/dist/v2/index.d.cts +4 -2
- package/dist/v2/index.d.mts +4 -2
- package/dist/v2/index.mjs +3 -1
- package/package.json +2 -2
- package/src/agent/__tests__/agent-test-helpers.ts +446 -0
- package/src/agent/__tests__/agent.test.ts +593 -0
- package/src/agent/__tests__/converter-aisdk.test.ts +692 -0
- package/src/agent/__tests__/converter-custom.test.ts +319 -0
- package/src/agent/__tests__/converter-tanstack-input.test.ts +211 -0
- package/src/agent/__tests__/converter-tanstack.test.ts +314 -0
- package/src/agent/__tests__/multimodal-tanstack.test.ts +284 -0
- package/src/agent/__tests__/test-helpers.ts +12 -8
- package/src/agent/converters/aisdk.ts +326 -0
- package/src/agent/converters/index.ts +7 -0
- package/src/agent/converters/tanstack.ts +286 -0
- package/src/agent/index.ts +245 -26
- package/src/lib/integrations/nextjs/pages-router.ts +1 -0
- package/src/lib/runtime/copilot-runtime.ts +21 -12
- package/src/lib/runtime/mcp-tools-utils.ts +1 -1
- package/src/service-adapters/anthropic/utils.ts +1 -1
- package/src/service-adapters/openai/utils.ts +1 -1
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { EventType } from "@ag-ui/client";
|
|
3
|
+
import {
|
|
4
|
+
createAgent,
|
|
5
|
+
createDefaultInput,
|
|
6
|
+
collectEvents,
|
|
7
|
+
expectLifecycleWrapped,
|
|
8
|
+
expectEventSequence,
|
|
9
|
+
eventField,
|
|
10
|
+
tanstackTextChunk,
|
|
11
|
+
tanstackToolCallStart,
|
|
12
|
+
tanstackToolCallArgs,
|
|
13
|
+
tanstackToolCallEnd,
|
|
14
|
+
} from "./agent-test-helpers";
|
|
15
|
+
|
|
16
|
+
describe("TanStack AI converter (via Agent)", () => {
|
|
17
|
+
// -------------------------------------------------------------------------
|
|
18
|
+
// Text Events
|
|
19
|
+
// -------------------------------------------------------------------------
|
|
20
|
+
describe("Text Events", () => {
|
|
21
|
+
it("TEXT_MESSAGE_CONTENT chunk produces TEXT_MESSAGE_CHUNK with role assistant and correct delta", async () => {
|
|
22
|
+
const agent = createAgent("tanstack", [tanstackTextChunk("Hello world")]);
|
|
23
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
24
|
+
|
|
25
|
+
expectLifecycleWrapped(events);
|
|
26
|
+
|
|
27
|
+
const textEvents = events.filter(
|
|
28
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
29
|
+
);
|
|
30
|
+
expect(textEvents).toHaveLength(1);
|
|
31
|
+
|
|
32
|
+
expect(eventField<string>(textEvents[0], "role")).toBe("assistant");
|
|
33
|
+
expect(eventField<string>(textEvents[0], "delta")).toBe("Hello world");
|
|
34
|
+
expect(eventField<string>(textEvents[0], "messageId")).toBeDefined();
|
|
35
|
+
expect(typeof eventField<string>(textEvents[0], "messageId")).toBe(
|
|
36
|
+
"string",
|
|
37
|
+
);
|
|
38
|
+
expect(
|
|
39
|
+
eventField<string>(textEvents[0], "messageId").length,
|
|
40
|
+
).toBeGreaterThan(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("multiple text chunks share the same messageId", async () => {
|
|
44
|
+
const agent = createAgent("tanstack", [
|
|
45
|
+
tanstackTextChunk("Hello "),
|
|
46
|
+
tanstackTextChunk("world"),
|
|
47
|
+
tanstackTextChunk("!"),
|
|
48
|
+
]);
|
|
49
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
50
|
+
|
|
51
|
+
expectLifecycleWrapped(events);
|
|
52
|
+
|
|
53
|
+
const textEvents = events.filter(
|
|
54
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
55
|
+
);
|
|
56
|
+
expect(textEvents).toHaveLength(3);
|
|
57
|
+
|
|
58
|
+
const messageIds = new Set(
|
|
59
|
+
textEvents.map((e) => eventField<string>(e, "messageId")),
|
|
60
|
+
);
|
|
61
|
+
expect(messageIds.size).toBe(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("empty stream produces only RUN_STARTED + RUN_FINISHED", async () => {
|
|
65
|
+
const agent = createAgent("tanstack", []);
|
|
66
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
67
|
+
|
|
68
|
+
expectEventSequence(events, [
|
|
69
|
+
EventType.RUN_STARTED,
|
|
70
|
+
EventType.RUN_FINISHED,
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// -------------------------------------------------------------------------
|
|
76
|
+
// Tool Call Events
|
|
77
|
+
// -------------------------------------------------------------------------
|
|
78
|
+
describe("Tool Call Events", () => {
|
|
79
|
+
it("full tool call lifecycle produces START, ARGS, END events in order", async () => {
|
|
80
|
+
const agent = createAgent("tanstack", [
|
|
81
|
+
tanstackToolCallStart("tc-1", "myTool"),
|
|
82
|
+
tanstackToolCallArgs("tc-1", '{"key":'),
|
|
83
|
+
tanstackToolCallArgs("tc-1", '"value"}'),
|
|
84
|
+
tanstackToolCallEnd("tc-1"),
|
|
85
|
+
]);
|
|
86
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
87
|
+
|
|
88
|
+
expectLifecycleWrapped(events);
|
|
89
|
+
expectEventSequence(events, [
|
|
90
|
+
EventType.RUN_STARTED,
|
|
91
|
+
EventType.TOOL_CALL_START,
|
|
92
|
+
EventType.TOOL_CALL_ARGS,
|
|
93
|
+
EventType.TOOL_CALL_ARGS,
|
|
94
|
+
EventType.TOOL_CALL_END,
|
|
95
|
+
EventType.RUN_FINISHED,
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
expect(eventField<string>(events[1], "toolCallId")).toBe("tc-1");
|
|
99
|
+
expect(eventField<string>(events[1], "toolCallName")).toBe("myTool");
|
|
100
|
+
|
|
101
|
+
expect(eventField<string>(events[2], "toolCallId")).toBe("tc-1");
|
|
102
|
+
expect(eventField<string>(events[2], "delta")).toBe('{"key":');
|
|
103
|
+
|
|
104
|
+
expect(eventField<string>(events[3], "toolCallId")).toBe("tc-1");
|
|
105
|
+
expect(eventField<string>(events[3], "delta")).toBe('"value"}');
|
|
106
|
+
|
|
107
|
+
expect(eventField<string>(events[4], "toolCallId")).toBe("tc-1");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("TOOL_CALL_START sets parentMessageId", async () => {
|
|
111
|
+
const agent = createAgent("tanstack", [
|
|
112
|
+
tanstackTextChunk("before"),
|
|
113
|
+
tanstackToolCallStart("tc-1", "myTool"),
|
|
114
|
+
tanstackToolCallEnd("tc-1"),
|
|
115
|
+
]);
|
|
116
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
117
|
+
|
|
118
|
+
const textEvent = events.find(
|
|
119
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
120
|
+
)!;
|
|
121
|
+
const toolStartEvent = events.find(
|
|
122
|
+
(e) => e.type === EventType.TOOL_CALL_START,
|
|
123
|
+
)!;
|
|
124
|
+
|
|
125
|
+
expect(
|
|
126
|
+
eventField<string>(toolStartEvent, "parentMessageId"),
|
|
127
|
+
).toBeDefined();
|
|
128
|
+
expect(eventField<string>(toolStartEvent, "parentMessageId")).toBe(
|
|
129
|
+
eventField<string>(textEvent, "messageId"),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("multiple tool calls in sequence each get correct events", async () => {
|
|
134
|
+
const agent = createAgent("tanstack", [
|
|
135
|
+
tanstackToolCallStart("tc-1", "toolA"),
|
|
136
|
+
tanstackToolCallArgs("tc-1", '{"a":1}'),
|
|
137
|
+
tanstackToolCallEnd("tc-1"),
|
|
138
|
+
tanstackToolCallStart("tc-2", "toolB"),
|
|
139
|
+
tanstackToolCallArgs("tc-2", '{"b":2}'),
|
|
140
|
+
tanstackToolCallEnd("tc-2"),
|
|
141
|
+
]);
|
|
142
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
143
|
+
|
|
144
|
+
expectLifecycleWrapped(events);
|
|
145
|
+
expectEventSequence(events, [
|
|
146
|
+
EventType.RUN_STARTED,
|
|
147
|
+
EventType.TOOL_CALL_START,
|
|
148
|
+
EventType.TOOL_CALL_ARGS,
|
|
149
|
+
EventType.TOOL_CALL_END,
|
|
150
|
+
EventType.TOOL_CALL_START,
|
|
151
|
+
EventType.TOOL_CALL_ARGS,
|
|
152
|
+
EventType.TOOL_CALL_END,
|
|
153
|
+
EventType.RUN_FINISHED,
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
// Verify first tool call
|
|
157
|
+
expect(eventField<string>(events[1], "toolCallId")).toBe("tc-1");
|
|
158
|
+
expect(eventField<string>(events[1], "toolCallName")).toBe("toolA");
|
|
159
|
+
|
|
160
|
+
expect(eventField<string>(events[2], "toolCallId")).toBe("tc-1");
|
|
161
|
+
|
|
162
|
+
expect(eventField<string>(events[3], "toolCallId")).toBe("tc-1");
|
|
163
|
+
|
|
164
|
+
// Verify second tool call
|
|
165
|
+
expect(eventField<string>(events[4], "toolCallId")).toBe("tc-2");
|
|
166
|
+
expect(eventField<string>(events[4], "toolCallName")).toBe("toolB");
|
|
167
|
+
|
|
168
|
+
expect(eventField<string>(events[5], "toolCallId")).toBe("tc-2");
|
|
169
|
+
|
|
170
|
+
expect(eventField<string>(events[6], "toolCallId")).toBe("tc-2");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("tool call with no ARGS chunks produces only START + END", async () => {
|
|
174
|
+
const agent = createAgent("tanstack", [
|
|
175
|
+
tanstackToolCallStart("tc-1", "noArgsTool"),
|
|
176
|
+
tanstackToolCallEnd("tc-1"),
|
|
177
|
+
]);
|
|
178
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
179
|
+
|
|
180
|
+
expectLifecycleWrapped(events);
|
|
181
|
+
expectEventSequence(events, [
|
|
182
|
+
EventType.RUN_STARTED,
|
|
183
|
+
EventType.TOOL_CALL_START,
|
|
184
|
+
EventType.TOOL_CALL_END,
|
|
185
|
+
EventType.RUN_FINISHED,
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// -------------------------------------------------------------------------
|
|
191
|
+
// Tool Call Result Events
|
|
192
|
+
// -------------------------------------------------------------------------
|
|
193
|
+
describe("Tool Call Result Events", () => {
|
|
194
|
+
it("TOOL_CALL_RESULT chunk produces TOOL_CALL_RESULT event with correct content", async () => {
|
|
195
|
+
const agent = createAgent("tanstack", [
|
|
196
|
+
tanstackToolCallStart("tc-1", "myTool"),
|
|
197
|
+
tanstackToolCallArgs("tc-1", '{"key":"value"}'),
|
|
198
|
+
tanstackToolCallEnd("tc-1"),
|
|
199
|
+
{
|
|
200
|
+
type: "TOOL_CALL_RESULT",
|
|
201
|
+
toolCallId: "tc-1",
|
|
202
|
+
content: JSON.stringify({ result: "ok" }),
|
|
203
|
+
},
|
|
204
|
+
]);
|
|
205
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
206
|
+
|
|
207
|
+
expectLifecycleWrapped(events);
|
|
208
|
+
|
|
209
|
+
const resultEvents = events.filter(
|
|
210
|
+
(e) => e.type === EventType.TOOL_CALL_RESULT,
|
|
211
|
+
);
|
|
212
|
+
expect(resultEvents).toHaveLength(1);
|
|
213
|
+
expect(eventField<string>(resultEvents[0], "toolCallId")).toBe("tc-1");
|
|
214
|
+
expect(eventField<string>(resultEvents[0], "role")).toBe("tool");
|
|
215
|
+
expect(
|
|
216
|
+
JSON.parse(eventField<string>(resultEvents[0], "content")),
|
|
217
|
+
).toEqual({ result: "ok" });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("TOOL_CALL_RESULT with object content serializes to JSON", async () => {
|
|
221
|
+
const agent = createAgent("tanstack", [
|
|
222
|
+
tanstackToolCallStart("tc-2", "myTool"),
|
|
223
|
+
tanstackToolCallEnd("tc-2"),
|
|
224
|
+
{
|
|
225
|
+
type: "TOOL_CALL_RESULT",
|
|
226
|
+
toolCallId: "tc-2",
|
|
227
|
+
result: { data: 42 },
|
|
228
|
+
},
|
|
229
|
+
]);
|
|
230
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
231
|
+
|
|
232
|
+
const resultEvents = events.filter(
|
|
233
|
+
(e) => e.type === EventType.TOOL_CALL_RESULT,
|
|
234
|
+
);
|
|
235
|
+
expect(resultEvents).toHaveLength(1);
|
|
236
|
+
expect(
|
|
237
|
+
JSON.parse(eventField<string>(resultEvents[0], "content")),
|
|
238
|
+
).toEqual({ data: 42 });
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// -------------------------------------------------------------------------
|
|
243
|
+
// Mixed Content
|
|
244
|
+
// -------------------------------------------------------------------------
|
|
245
|
+
describe("Mixed Content", () => {
|
|
246
|
+
it("text interleaved with tool calls produces correct event types and order", async () => {
|
|
247
|
+
const agent = createAgent("tanstack", [
|
|
248
|
+
tanstackTextChunk("Let me help. "),
|
|
249
|
+
tanstackToolCallStart("tc-1", "search"),
|
|
250
|
+
tanstackToolCallArgs("tc-1", '{"q":"test"}'),
|
|
251
|
+
tanstackToolCallEnd("tc-1"),
|
|
252
|
+
tanstackTextChunk("Here are the results."),
|
|
253
|
+
]);
|
|
254
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
255
|
+
|
|
256
|
+
expectLifecycleWrapped(events);
|
|
257
|
+
expectEventSequence(events, [
|
|
258
|
+
EventType.RUN_STARTED,
|
|
259
|
+
EventType.TEXT_MESSAGE_CHUNK,
|
|
260
|
+
EventType.TOOL_CALL_START,
|
|
261
|
+
EventType.TOOL_CALL_ARGS,
|
|
262
|
+
EventType.TOOL_CALL_END,
|
|
263
|
+
EventType.TEXT_MESSAGE_CHUNK,
|
|
264
|
+
EventType.RUN_FINISHED,
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
// Verify content of text events
|
|
268
|
+
const textEvents = events.filter(
|
|
269
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
270
|
+
);
|
|
271
|
+
expect(eventField<string>(textEvents[0], "delta")).toBe("Let me help. ");
|
|
272
|
+
expect(eventField<string>(textEvents[1], "delta")).toBe(
|
|
273
|
+
"Here are the results.",
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// -------------------------------------------------------------------------
|
|
279
|
+
// Edge Cases
|
|
280
|
+
// -------------------------------------------------------------------------
|
|
281
|
+
describe("Edge Cases", () => {
|
|
282
|
+
it("unknown chunk types are silently ignored", async () => {
|
|
283
|
+
const agent = createAgent("tanstack", [
|
|
284
|
+
tanstackTextChunk("hello"),
|
|
285
|
+
{ type: "SOME_UNKNOWN_TYPE", data: "foo" },
|
|
286
|
+
{ type: "ANOTHER_MYSTERY", value: 42 },
|
|
287
|
+
tanstackTextChunk(" world"),
|
|
288
|
+
]);
|
|
289
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
290
|
+
|
|
291
|
+
expectLifecycleWrapped(events);
|
|
292
|
+
expectEventSequence(events, [
|
|
293
|
+
EventType.RUN_STARTED,
|
|
294
|
+
EventType.TEXT_MESSAGE_CHUNK,
|
|
295
|
+
EventType.TEXT_MESSAGE_CHUNK,
|
|
296
|
+
EventType.RUN_FINISHED,
|
|
297
|
+
]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("large deltas (100k chars) are passed through", async () => {
|
|
301
|
+
const largeDelta = "x".repeat(100_000);
|
|
302
|
+
const agent = createAgent("tanstack", [tanstackTextChunk(largeDelta)]);
|
|
303
|
+
const events = await collectEvents(agent.run(createDefaultInput()));
|
|
304
|
+
|
|
305
|
+
expectLifecycleWrapped(events);
|
|
306
|
+
|
|
307
|
+
const textEvent = events.find(
|
|
308
|
+
(e) => e.type === EventType.TEXT_MESSAGE_CHUNK,
|
|
309
|
+
)!;
|
|
310
|
+
expect(eventField<string>(textEvent, "delta")).toBe(largeDelta);
|
|
311
|
+
expect(eventField<string>(textEvent, "delta").length).toBe(100_000);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { convertInputToTanStackAI } from "../converters/tanstack";
|
|
3
|
+
import { createDefaultInput } from "./agent-test-helpers";
|
|
4
|
+
import type { Message, InputContent } from "@ag-ui/client";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper: build a user message with the given content parts, run through
|
|
8
|
+
* convertInputToTanStackAI, and return the first converted message for assertion.
|
|
9
|
+
*/
|
|
10
|
+
function convertUserContent(content: string | InputContent[]) {
|
|
11
|
+
const input = createDefaultInput({
|
|
12
|
+
messages: [{ role: "user", content } as Message],
|
|
13
|
+
});
|
|
14
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
15
|
+
return messages[0];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Shorthand factories for AG-UI input parts */
|
|
19
|
+
function dataSource(value: string, mimeType: string) {
|
|
20
|
+
return { type: "data" as const, value, mimeType };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function urlSource(value: string, mimeType?: string) {
|
|
24
|
+
return { type: "url" as const, value, ...(mimeType ? { mimeType } : {}) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("convertInputToTanStackAI — multimodal", () => {
|
|
28
|
+
it("passes through plain string user content", () => {
|
|
29
|
+
const result = convertUserContent("Hello");
|
|
30
|
+
expect(result.content).toBe("Hello");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("converts text-only InputContent[] to TanStack TextPart array", () => {
|
|
34
|
+
const result = convertUserContent([{ type: "text", text: "Hello world" }]);
|
|
35
|
+
expect(result.content).toEqual([{ type: "text", content: "Hello world" }]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("converts ImageInputPart with data source", () => {
|
|
39
|
+
const result = convertUserContent([
|
|
40
|
+
{ type: "text", text: "What is this?" },
|
|
41
|
+
{ type: "image", source: dataSource("iVBORw0KGgo=", "image/png") },
|
|
42
|
+
]);
|
|
43
|
+
expect(result.content).toEqual([
|
|
44
|
+
{ type: "text", content: "What is this?" },
|
|
45
|
+
{
|
|
46
|
+
type: "image",
|
|
47
|
+
source: { type: "data", value: "iVBORw0KGgo=", mimeType: "image/png" },
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("converts ImageInputPart with url source", () => {
|
|
53
|
+
const result = convertUserContent([
|
|
54
|
+
{
|
|
55
|
+
type: "image",
|
|
56
|
+
source: urlSource("https://example.com/photo.jpg", "image/jpeg"),
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
expect(result.content).toEqual([
|
|
60
|
+
{
|
|
61
|
+
type: "image",
|
|
62
|
+
source: {
|
|
63
|
+
type: "url",
|
|
64
|
+
value: "https://example.com/photo.jpg",
|
|
65
|
+
mimeType: "image/jpeg",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("converts AudioInputPart", () => {
|
|
72
|
+
const result = convertUserContent([
|
|
73
|
+
{ type: "audio", source: dataSource("base64audiodata", "audio/mp3") },
|
|
74
|
+
]);
|
|
75
|
+
expect(result.content).toEqual([
|
|
76
|
+
{
|
|
77
|
+
type: "audio",
|
|
78
|
+
source: {
|
|
79
|
+
type: "data",
|
|
80
|
+
value: "base64audiodata",
|
|
81
|
+
mimeType: "audio/mp3",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("converts VideoInputPart with url source", () => {
|
|
88
|
+
const result = convertUserContent([
|
|
89
|
+
{
|
|
90
|
+
type: "video",
|
|
91
|
+
source: urlSource("https://example.com/video.mp4", "video/mp4"),
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
expect(result.content).toEqual([
|
|
95
|
+
{
|
|
96
|
+
type: "video",
|
|
97
|
+
source: {
|
|
98
|
+
type: "url",
|
|
99
|
+
value: "https://example.com/video.mp4",
|
|
100
|
+
mimeType: "video/mp4",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("converts DocumentInputPart", () => {
|
|
107
|
+
const result = convertUserContent([
|
|
108
|
+
{
|
|
109
|
+
type: "document",
|
|
110
|
+
source: dataSource("base64pdfdata", "application/pdf"),
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
expect(result.content).toEqual([
|
|
114
|
+
{
|
|
115
|
+
type: "document",
|
|
116
|
+
source: {
|
|
117
|
+
type: "data",
|
|
118
|
+
value: "base64pdfdata",
|
|
119
|
+
mimeType: "application/pdf",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("handles mixed text and multimodal parts", () => {
|
|
126
|
+
const result = convertUserContent([
|
|
127
|
+
{ type: "text", text: "Analyze these:" },
|
|
128
|
+
{ type: "image", source: dataSource("imgdata", "image/png") },
|
|
129
|
+
{
|
|
130
|
+
type: "document",
|
|
131
|
+
source: dataSource("docdata", "application/pdf"),
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
expect(result.content).toEqual([
|
|
135
|
+
{ type: "text", content: "Analyze these:" },
|
|
136
|
+
{
|
|
137
|
+
type: "image",
|
|
138
|
+
source: { type: "data", value: "imgdata", mimeType: "image/png" },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: "document",
|
|
142
|
+
source: {
|
|
143
|
+
type: "data",
|
|
144
|
+
value: "docdata",
|
|
145
|
+
mimeType: "application/pdf",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("returns empty string for empty content array", () => {
|
|
152
|
+
const result = convertUserContent([]);
|
|
153
|
+
expect(result.content).toBe("");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("preserves empty string text parts", () => {
|
|
157
|
+
const result = convertUserContent([{ type: "text", text: "" }]);
|
|
158
|
+
expect(result.content).toEqual([{ type: "text", content: "" }]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("skips parts with missing source without crashing", () => {
|
|
162
|
+
const result = convertUserContent([
|
|
163
|
+
{ type: "text", text: "check this" },
|
|
164
|
+
{ type: "image" } as any,
|
|
165
|
+
]);
|
|
166
|
+
expect(result.content).toEqual([{ type: "text", content: "check this" }]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("silently skips unknown part types", () => {
|
|
170
|
+
const result = convertUserContent([
|
|
171
|
+
{ type: "text", text: "hello" },
|
|
172
|
+
{ type: "spreadsheet", source: dataSource("data", "text/csv") } as any,
|
|
173
|
+
]);
|
|
174
|
+
expect(result.content).toEqual([{ type: "text", content: "hello" }]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("returns null for null or undefined content", () => {
|
|
178
|
+
const nullInput = createDefaultInput({
|
|
179
|
+
messages: [{ role: "user", content: null } as unknown as Message],
|
|
180
|
+
});
|
|
181
|
+
const { messages: nullMessages } = convertInputToTanStackAI(nullInput);
|
|
182
|
+
expect(nullMessages[0].content).toBeNull();
|
|
183
|
+
|
|
184
|
+
const undefinedInput = createDefaultInput({
|
|
185
|
+
messages: [{ role: "user", content: undefined } as unknown as Message],
|
|
186
|
+
});
|
|
187
|
+
const { messages: undefinedMessages } =
|
|
188
|
+
convertInputToTanStackAI(undefinedInput);
|
|
189
|
+
expect(undefinedMessages[0].content).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("legacy BinaryInputContent backward compat", () => {
|
|
193
|
+
it("converts binary with image mimeType and data", () => {
|
|
194
|
+
const legacyPart = {
|
|
195
|
+
type: "binary",
|
|
196
|
+
mimeType: "image/jpeg",
|
|
197
|
+
data: "legacybase64",
|
|
198
|
+
};
|
|
199
|
+
const input = createDefaultInput({
|
|
200
|
+
messages: [
|
|
201
|
+
{
|
|
202
|
+
role: "user",
|
|
203
|
+
content: [legacyPart] as unknown as InputContent[],
|
|
204
|
+
} as Message,
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
208
|
+
expect(messages[0].content).toEqual([
|
|
209
|
+
{
|
|
210
|
+
type: "image",
|
|
211
|
+
source: {
|
|
212
|
+
type: "data",
|
|
213
|
+
value: "legacybase64",
|
|
214
|
+
mimeType: "image/jpeg",
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("skips binary with neither data nor url", () => {
|
|
221
|
+
const legacyPart = {
|
|
222
|
+
type: "binary",
|
|
223
|
+
mimeType: "image/png",
|
|
224
|
+
};
|
|
225
|
+
const input = createDefaultInput({
|
|
226
|
+
messages: [
|
|
227
|
+
{
|
|
228
|
+
role: "user",
|
|
229
|
+
content: [legacyPart] as unknown as InputContent[],
|
|
230
|
+
} as Message,
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
234
|
+
expect(messages[0].content).toBe("");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("converts binary with non-image mimeType and url", () => {
|
|
238
|
+
const legacyPart = {
|
|
239
|
+
type: "binary",
|
|
240
|
+
mimeType: "application/pdf",
|
|
241
|
+
url: "https://example.com/doc.pdf",
|
|
242
|
+
};
|
|
243
|
+
const input = createDefaultInput({
|
|
244
|
+
messages: [
|
|
245
|
+
{
|
|
246
|
+
role: "user",
|
|
247
|
+
content: [legacyPart] as unknown as InputContent[],
|
|
248
|
+
} as Message,
|
|
249
|
+
],
|
|
250
|
+
});
|
|
251
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
252
|
+
expect(messages[0].content).toEqual([
|
|
253
|
+
{
|
|
254
|
+
type: "document",
|
|
255
|
+
source: {
|
|
256
|
+
type: "url",
|
|
257
|
+
value: "https://example.com/doc.pdf",
|
|
258
|
+
mimeType: "application/pdf",
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
]);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("only converts user message content, not assistant messages", () => {
|
|
266
|
+
const input = createDefaultInput({
|
|
267
|
+
messages: [
|
|
268
|
+
{
|
|
269
|
+
role: "user",
|
|
270
|
+
content: [
|
|
271
|
+
{ type: "text", text: "Look at this" },
|
|
272
|
+
{ type: "image", source: dataSource("imgdata", "image/png") },
|
|
273
|
+
],
|
|
274
|
+
} as Message,
|
|
275
|
+
{ role: "assistant", content: "I see an image" } as Message,
|
|
276
|
+
],
|
|
277
|
+
});
|
|
278
|
+
const { messages } = convertInputToTanStackAI(input);
|
|
279
|
+
// User message should have array content
|
|
280
|
+
expect(Array.isArray(messages[0].content)).toBe(true);
|
|
281
|
+
// Assistant message should keep string content
|
|
282
|
+
expect(messages[1].content).toBe("I see an image");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -178,16 +178,20 @@ export async function collectEvents(
|
|
|
178
178
|
): Promise<BaseEvent[]> {
|
|
179
179
|
return new Promise((resolve, reject) => {
|
|
180
180
|
const events: BaseEvent[] = [];
|
|
181
|
-
const
|
|
182
|
-
next: (event) => events.push(event),
|
|
183
|
-
error: (err: unknown) => reject(err),
|
|
184
|
-
complete: () => resolve(events),
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Set a timeout to prevent hanging tests
|
|
188
|
-
setTimeout(() => {
|
|
181
|
+
const timeoutId = setTimeout(() => {
|
|
189
182
|
subscription.unsubscribe();
|
|
190
183
|
reject(new Error("Observable did not complete within timeout"));
|
|
191
184
|
}, 5000);
|
|
185
|
+
const subscription = observable.subscribe({
|
|
186
|
+
next: (event) => events.push(event),
|
|
187
|
+
error: (err: unknown) => {
|
|
188
|
+
clearTimeout(timeoutId);
|
|
189
|
+
reject(err);
|
|
190
|
+
},
|
|
191
|
+
complete: () => {
|
|
192
|
+
clearTimeout(timeoutId);
|
|
193
|
+
resolve(events);
|
|
194
|
+
},
|
|
195
|
+
});
|
|
192
196
|
});
|
|
193
197
|
}
|