@kernl-sdk/ai 0.1.1 → 0.1.2
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +9 -0
- package/dist/__tests__/integration.test.js +137 -0
- package/dist/__tests__/language-model.test.d.ts +2 -0
- package/dist/__tests__/language-model.test.d.ts.map +1 -0
- package/dist/__tests__/language-model.test.js +423 -0
- package/dist/convert/__tests__/message.test.js +1 -1
- package/dist/convert/message.js +1 -1
- package/dist/convert/response.js +1 -1
- package/dist/convert/stream.d.ts.map +1 -1
- package/dist/convert/stream.js +5 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/language-model.d.ts.map +1 -1
- package/dist/language-model.js +61 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/google.d.ts +1 -1
- package/dist/providers/google.js +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +1 -1
- package/package.json +4 -4
- package/src/__tests__/integration.test.ts +161 -0
- package/src/__tests__/language-model.test.ts +465 -0
- package/src/convert/__tests__/message.test.ts +1 -1
- package/src/convert/message.ts +1 -1
- package/src/convert/response.ts +1 -1
- package/src/convert/stream.ts +5 -3
- package/src/index.ts +1 -1
- package/src/language-model.ts +68 -1
- package/src/providers/anthropic.ts +1 -1
- package/src/providers/google.ts +1 -1
- package/src/providers/openai.ts +1 -1
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -221,6 +221,57 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
221
221
|
const finishEvents = events.filter((e) => e.kind === "finish");
|
|
222
222
|
expect(finishEvents.length).toBe(1);
|
|
223
223
|
});
|
|
224
|
+
it("should yield both delta events and complete Message items", async () => {
|
|
225
|
+
const events = [];
|
|
226
|
+
for await (const event of gpt4omini.stream({
|
|
227
|
+
input: [
|
|
228
|
+
{
|
|
229
|
+
kind: "message",
|
|
230
|
+
role: "user",
|
|
231
|
+
id: "msg-1",
|
|
232
|
+
content: [
|
|
233
|
+
{ kind: "text", text: "Say 'Hello World' and nothing else." },
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
settings: {
|
|
238
|
+
maxTokens: 50,
|
|
239
|
+
temperature: 0,
|
|
240
|
+
},
|
|
241
|
+
})) {
|
|
242
|
+
events.push(event);
|
|
243
|
+
}
|
|
244
|
+
expect(events.length).toBeGreaterThan(0);
|
|
245
|
+
// Should have text-delta events (for streaming UX)
|
|
246
|
+
const textDeltas = events.filter((e) => e.kind === "text-delta");
|
|
247
|
+
expect(textDeltas.length).toBeGreaterThan(0);
|
|
248
|
+
// Should have text-start event
|
|
249
|
+
const textStarts = events.filter((e) => e.kind === "text-start");
|
|
250
|
+
expect(textStarts.length).toBeGreaterThan(0);
|
|
251
|
+
// Should have text-end event
|
|
252
|
+
const textEnds = events.filter((e) => e.kind === "text-end");
|
|
253
|
+
expect(textEnds.length).toBeGreaterThan(0);
|
|
254
|
+
// Should have complete Message item (for history)
|
|
255
|
+
const messages = events.filter((e) => e.kind === "message");
|
|
256
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
257
|
+
const assistantMessage = messages[0];
|
|
258
|
+
expect(assistantMessage.role).toBe("assistant");
|
|
259
|
+
expect(assistantMessage.content).toBeDefined();
|
|
260
|
+
expect(assistantMessage.content.length).toBeGreaterThan(0);
|
|
261
|
+
// Message should have accumulated text from all deltas
|
|
262
|
+
const textContent = assistantMessage.content.find((c) => c.kind === "text");
|
|
263
|
+
expect(textContent).toBeDefined();
|
|
264
|
+
expect(textContent.text).toBeDefined();
|
|
265
|
+
expect(textContent.text.length).toBeGreaterThan(0);
|
|
266
|
+
// Verify accumulated text matches concatenated deltas
|
|
267
|
+
const accumulatedFromDeltas = textDeltas
|
|
268
|
+
.map((d) => d.text)
|
|
269
|
+
.join("");
|
|
270
|
+
expect(textContent.text).toBe(accumulatedFromDeltas);
|
|
271
|
+
// Should have finish event
|
|
272
|
+
const finishEvents = events.filter((e) => e.kind === "finish");
|
|
273
|
+
expect(finishEvents.length).toBe(1);
|
|
274
|
+
});
|
|
224
275
|
});
|
|
225
276
|
describe("tools", () => {
|
|
226
277
|
it("should call tools when requested", async () => {
|
|
@@ -350,6 +401,92 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
350
401
|
const toolCalls = response.content.filter((item) => item.kind === "tool-call");
|
|
351
402
|
expect(toolCalls.length).toBeGreaterThan(0);
|
|
352
403
|
});
|
|
404
|
+
it("should handle multi-turn conversation with tool results", async () => {
|
|
405
|
+
// First turn: get tool calls from the model
|
|
406
|
+
const firstResponse = await gpt4omini.generate({
|
|
407
|
+
input: [
|
|
408
|
+
{
|
|
409
|
+
kind: "message",
|
|
410
|
+
role: "user",
|
|
411
|
+
id: "msg-1",
|
|
412
|
+
content: [{ kind: "text", text: "What is 25 + 17?" }],
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
tools: [
|
|
416
|
+
{
|
|
417
|
+
kind: "function",
|
|
418
|
+
name: "calculate",
|
|
419
|
+
description: "Perform a mathematical calculation",
|
|
420
|
+
parameters: {
|
|
421
|
+
type: "object",
|
|
422
|
+
properties: {
|
|
423
|
+
expression: {
|
|
424
|
+
type: "string",
|
|
425
|
+
description: "The mathematical expression to evaluate",
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
required: ["expression"],
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
settings: {
|
|
433
|
+
maxTokens: 200,
|
|
434
|
+
temperature: 0,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
expect(firstResponse.content).toBeDefined();
|
|
438
|
+
// Extract tool calls
|
|
439
|
+
const toolCalls = firstResponse.content.filter((item) => item.kind === "tool-call");
|
|
440
|
+
expect(toolCalls.length).toBeGreaterThan(0);
|
|
441
|
+
const toolCall = toolCalls[0];
|
|
442
|
+
expect(toolCall.callId).toBeDefined();
|
|
443
|
+
expect(toolCall.toolId).toBe("calculate");
|
|
444
|
+
// Second turn: send tool results back to the model
|
|
445
|
+
const secondResponse = await gpt4omini.generate({
|
|
446
|
+
input: [
|
|
447
|
+
{
|
|
448
|
+
kind: "message",
|
|
449
|
+
role: "user",
|
|
450
|
+
id: "msg-1",
|
|
451
|
+
content: [{ kind: "text", text: "What is 25 + 17?" }],
|
|
452
|
+
},
|
|
453
|
+
...firstResponse.content,
|
|
454
|
+
{
|
|
455
|
+
kind: "tool-result",
|
|
456
|
+
callId: toolCall.callId,
|
|
457
|
+
toolId: toolCall.toolId,
|
|
458
|
+
state: "completed",
|
|
459
|
+
result: { answer: 42 },
|
|
460
|
+
error: null,
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
tools: [
|
|
464
|
+
{
|
|
465
|
+
kind: "function",
|
|
466
|
+
name: "calculate",
|
|
467
|
+
description: "Perform a mathematical calculation",
|
|
468
|
+
parameters: {
|
|
469
|
+
type: "object",
|
|
470
|
+
properties: {
|
|
471
|
+
expression: {
|
|
472
|
+
type: "string",
|
|
473
|
+
description: "The mathematical expression to evaluate",
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
required: ["expression"],
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
],
|
|
480
|
+
settings: {
|
|
481
|
+
maxTokens: 200,
|
|
482
|
+
temperature: 0,
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
expect(secondResponse.content).toBeDefined();
|
|
486
|
+
// Should have an assistant message with the final answer
|
|
487
|
+
const messages = secondResponse.content.filter((item) => item.kind === "message" && item.role === "assistant");
|
|
488
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
489
|
+
});
|
|
353
490
|
});
|
|
354
491
|
describe("validation", () => {
|
|
355
492
|
it("should throw error for invalid maxTokens", async () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-model.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/language-model.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { AISDKLanguageModel } from "../language-model";
|
|
3
|
+
/**
|
|
4
|
+
* Unit tests for AISDKLanguageModel stream accumulation behavior
|
|
5
|
+
*/
|
|
6
|
+
describe("AISDKLanguageModel", () => {
|
|
7
|
+
describe("stream - delta accumulation", () => {
|
|
8
|
+
it("should yield delta events and complete Message on text-end", async () => {
|
|
9
|
+
// Mock the underlying AI SDK model
|
|
10
|
+
const mockModel = {
|
|
11
|
+
provider: "test",
|
|
12
|
+
modelId: "test-model",
|
|
13
|
+
doStream: vi.fn().mockResolvedValue({
|
|
14
|
+
stream: new ReadableStream({
|
|
15
|
+
start(controller) {
|
|
16
|
+
const parts = [
|
|
17
|
+
{
|
|
18
|
+
type: "text-start",
|
|
19
|
+
id: "text-1",
|
|
20
|
+
providerMetadata: undefined,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: "text-delta",
|
|
24
|
+
id: "text-1",
|
|
25
|
+
delta: "Hello",
|
|
26
|
+
providerMetadata: undefined,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "text-delta",
|
|
30
|
+
id: "text-1",
|
|
31
|
+
delta: " ",
|
|
32
|
+
providerMetadata: undefined,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: "text-delta",
|
|
36
|
+
id: "text-1",
|
|
37
|
+
delta: "World",
|
|
38
|
+
providerMetadata: undefined,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "text-end",
|
|
42
|
+
id: "text-1",
|
|
43
|
+
providerMetadata: { test: { foo: "bar" } },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: "finish",
|
|
47
|
+
finishReason: "stop",
|
|
48
|
+
usage: {
|
|
49
|
+
inputTokens: 5,
|
|
50
|
+
outputTokens: 10,
|
|
51
|
+
totalTokens: 15,
|
|
52
|
+
},
|
|
53
|
+
providerMetadata: undefined,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
for (const part of parts) {
|
|
57
|
+
controller.enqueue(part);
|
|
58
|
+
}
|
|
59
|
+
controller.close();
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
const model = new AISDKLanguageModel(mockModel);
|
|
65
|
+
const events = [];
|
|
66
|
+
for await (const event of model.stream({
|
|
67
|
+
input: [
|
|
68
|
+
{
|
|
69
|
+
kind: "message",
|
|
70
|
+
role: "user",
|
|
71
|
+
id: "msg-1",
|
|
72
|
+
content: [{ kind: "text", text: "Hi" }],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
settings: {},
|
|
76
|
+
})) {
|
|
77
|
+
events.push(event);
|
|
78
|
+
}
|
|
79
|
+
// Should have: start, 3 deltas, end, complete Message, finish
|
|
80
|
+
expect(events).toHaveLength(7);
|
|
81
|
+
// Check delta events
|
|
82
|
+
expect(events[0]).toMatchObject({ kind: "text-start", id: "text-1" });
|
|
83
|
+
expect(events[1]).toMatchObject({
|
|
84
|
+
kind: "text-delta",
|
|
85
|
+
id: "text-1",
|
|
86
|
+
text: "Hello",
|
|
87
|
+
});
|
|
88
|
+
expect(events[2]).toMatchObject({
|
|
89
|
+
kind: "text-delta",
|
|
90
|
+
id: "text-1",
|
|
91
|
+
text: " ",
|
|
92
|
+
});
|
|
93
|
+
expect(events[3]).toMatchObject({
|
|
94
|
+
kind: "text-delta",
|
|
95
|
+
id: "text-1",
|
|
96
|
+
text: "World",
|
|
97
|
+
});
|
|
98
|
+
// Check complete Message item (yielded before end event)
|
|
99
|
+
const messageEvent = events[4];
|
|
100
|
+
expect(messageEvent.kind).toBe("message");
|
|
101
|
+
expect(messageEvent).toMatchObject({
|
|
102
|
+
kind: "message",
|
|
103
|
+
role: "assistant",
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
kind: "text",
|
|
107
|
+
text: "Hello World", // Accumulated text
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
providerMetadata: { test: { foo: "bar" } }, // From end event
|
|
111
|
+
});
|
|
112
|
+
expect(messageEvent.id).toBeDefined();
|
|
113
|
+
// Check end event (yielded after Message)
|
|
114
|
+
expect(events[5]).toMatchObject({ kind: "text-end", id: "text-1" });
|
|
115
|
+
// Check finish event
|
|
116
|
+
expect(events[6]).toMatchObject({ kind: "finish" });
|
|
117
|
+
});
|
|
118
|
+
it("should yield delta events and complete Reasoning on reasoning-end", async () => {
|
|
119
|
+
const mockModel = {
|
|
120
|
+
provider: "test",
|
|
121
|
+
modelId: "test-model",
|
|
122
|
+
doStream: vi.fn().mockResolvedValue({
|
|
123
|
+
stream: new ReadableStream({
|
|
124
|
+
start(controller) {
|
|
125
|
+
const parts = [
|
|
126
|
+
{
|
|
127
|
+
type: "reasoning-start",
|
|
128
|
+
id: "reason-1",
|
|
129
|
+
providerMetadata: undefined,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: "reasoning-delta",
|
|
133
|
+
id: "reason-1",
|
|
134
|
+
delta: "Let me think",
|
|
135
|
+
providerMetadata: undefined,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: "reasoning-delta",
|
|
139
|
+
id: "reason-1",
|
|
140
|
+
delta: " about this",
|
|
141
|
+
providerMetadata: undefined,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: "reasoning-end",
|
|
145
|
+
id: "reason-1",
|
|
146
|
+
providerMetadata: { test: { reasoning: true } },
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: "finish",
|
|
150
|
+
finishReason: "stop",
|
|
151
|
+
usage: {
|
|
152
|
+
inputTokens: 5,
|
|
153
|
+
outputTokens: 20,
|
|
154
|
+
totalTokens: 25,
|
|
155
|
+
},
|
|
156
|
+
providerMetadata: undefined,
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
for (const part of parts) {
|
|
160
|
+
controller.enqueue(part);
|
|
161
|
+
}
|
|
162
|
+
controller.close();
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
}),
|
|
166
|
+
};
|
|
167
|
+
const model = new AISDKLanguageModel(mockModel);
|
|
168
|
+
const events = [];
|
|
169
|
+
for await (const event of model.stream({
|
|
170
|
+
input: [
|
|
171
|
+
{
|
|
172
|
+
kind: "message",
|
|
173
|
+
role: "user",
|
|
174
|
+
id: "msg-1",
|
|
175
|
+
content: [{ kind: "text", text: "Think about this" }],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
settings: {},
|
|
179
|
+
})) {
|
|
180
|
+
events.push(event);
|
|
181
|
+
}
|
|
182
|
+
// Should have: start, 2 deltas, end, complete Reasoning, finish
|
|
183
|
+
expect(events).toHaveLength(6);
|
|
184
|
+
// Check delta events
|
|
185
|
+
expect(events[0]).toMatchObject({
|
|
186
|
+
kind: "reasoning-start",
|
|
187
|
+
id: "reason-1",
|
|
188
|
+
});
|
|
189
|
+
expect(events[1]).toMatchObject({
|
|
190
|
+
kind: "reasoning-delta",
|
|
191
|
+
id: "reason-1",
|
|
192
|
+
text: "Let me think",
|
|
193
|
+
});
|
|
194
|
+
expect(events[2]).toMatchObject({
|
|
195
|
+
kind: "reasoning-delta",
|
|
196
|
+
id: "reason-1",
|
|
197
|
+
text: " about this",
|
|
198
|
+
});
|
|
199
|
+
// Check complete Reasoning item (yielded before end event)
|
|
200
|
+
const reasoningEvent = events[3];
|
|
201
|
+
expect(reasoningEvent.kind).toBe("reasoning");
|
|
202
|
+
expect(reasoningEvent).toMatchObject({
|
|
203
|
+
kind: "reasoning",
|
|
204
|
+
text: "Let me think about this", // Accumulated text
|
|
205
|
+
providerMetadata: { test: { reasoning: true } }, // From end event
|
|
206
|
+
});
|
|
207
|
+
expect(reasoningEvent.id).toBeDefined();
|
|
208
|
+
// Check end event (yielded after Reasoning)
|
|
209
|
+
expect(events[4]).toMatchObject({
|
|
210
|
+
kind: "reasoning-end",
|
|
211
|
+
id: "reason-1",
|
|
212
|
+
});
|
|
213
|
+
// Check finish event
|
|
214
|
+
expect(events[5]).toMatchObject({ kind: "finish" });
|
|
215
|
+
});
|
|
216
|
+
it("should handle multiple text blocks with different IDs", async () => {
|
|
217
|
+
const mockModel = {
|
|
218
|
+
provider: "test",
|
|
219
|
+
modelId: "test-model",
|
|
220
|
+
doStream: vi.fn().mockResolvedValue({
|
|
221
|
+
stream: new ReadableStream({
|
|
222
|
+
start(controller) {
|
|
223
|
+
const parts = [
|
|
224
|
+
{ type: "text-start", id: "text-1", providerMetadata: undefined },
|
|
225
|
+
{
|
|
226
|
+
type: "text-delta",
|
|
227
|
+
id: "text-1",
|
|
228
|
+
delta: "First",
|
|
229
|
+
providerMetadata: undefined,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
type: "text-end",
|
|
233
|
+
id: "text-1",
|
|
234
|
+
providerMetadata: { test: { order: 1 } },
|
|
235
|
+
},
|
|
236
|
+
{ type: "text-start", id: "text-2", providerMetadata: undefined },
|
|
237
|
+
{
|
|
238
|
+
type: "text-delta",
|
|
239
|
+
id: "text-2",
|
|
240
|
+
delta: "Second",
|
|
241
|
+
providerMetadata: undefined,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
type: "text-end",
|
|
245
|
+
id: "text-2",
|
|
246
|
+
providerMetadata: { test: { order: 2 } },
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
type: "finish",
|
|
250
|
+
finishReason: "stop",
|
|
251
|
+
usage: {
|
|
252
|
+
inputTokens: 5,
|
|
253
|
+
outputTokens: 10,
|
|
254
|
+
totalTokens: 15,
|
|
255
|
+
},
|
|
256
|
+
providerMetadata: undefined,
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
for (const part of parts) {
|
|
260
|
+
controller.enqueue(part);
|
|
261
|
+
}
|
|
262
|
+
controller.close();
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
}),
|
|
266
|
+
};
|
|
267
|
+
const model = new AISDKLanguageModel(mockModel);
|
|
268
|
+
const events = [];
|
|
269
|
+
for await (const event of model.stream({
|
|
270
|
+
input: [
|
|
271
|
+
{
|
|
272
|
+
kind: "message",
|
|
273
|
+
role: "user",
|
|
274
|
+
id: "msg-1",
|
|
275
|
+
content: [{ kind: "text", text: "Hi" }],
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
settings: {},
|
|
279
|
+
})) {
|
|
280
|
+
events.push(event);
|
|
281
|
+
}
|
|
282
|
+
// Check we got two separate Message items
|
|
283
|
+
const messageEvents = events.filter((e) => e.kind === "message");
|
|
284
|
+
expect(messageEvents).toHaveLength(2);
|
|
285
|
+
expect(messageEvents[0]).toMatchObject({
|
|
286
|
+
kind: "message",
|
|
287
|
+
role: "assistant",
|
|
288
|
+
content: [{ kind: "text", text: "First" }],
|
|
289
|
+
providerMetadata: { test: { order: 1 } },
|
|
290
|
+
});
|
|
291
|
+
expect(messageEvents[1]).toMatchObject({
|
|
292
|
+
kind: "message",
|
|
293
|
+
role: "assistant",
|
|
294
|
+
content: [{ kind: "text", text: "Second" }],
|
|
295
|
+
providerMetadata: { test: { order: 2 } },
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
it("should use metadata from end event (last-wins)", async () => {
|
|
299
|
+
const mockModel = {
|
|
300
|
+
provider: "test",
|
|
301
|
+
modelId: "test-model",
|
|
302
|
+
doStream: vi.fn().mockResolvedValue({
|
|
303
|
+
stream: new ReadableStream({
|
|
304
|
+
start(controller) {
|
|
305
|
+
const parts = [
|
|
306
|
+
{
|
|
307
|
+
type: "text-start",
|
|
308
|
+
id: "text-1",
|
|
309
|
+
providerMetadata: { test: { version: "start" } },
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
type: "text-delta",
|
|
313
|
+
id: "text-1",
|
|
314
|
+
delta: "Test",
|
|
315
|
+
providerMetadata: { test: { version: "delta" } },
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
type: "text-end",
|
|
319
|
+
id: "text-1",
|
|
320
|
+
providerMetadata: { test: { version: "end" } },
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
type: "finish",
|
|
324
|
+
finishReason: "stop",
|
|
325
|
+
usage: {
|
|
326
|
+
inputTokens: 5,
|
|
327
|
+
outputTokens: 10,
|
|
328
|
+
totalTokens: 15,
|
|
329
|
+
},
|
|
330
|
+
providerMetadata: undefined,
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
for (const part of parts) {
|
|
334
|
+
controller.enqueue(part);
|
|
335
|
+
}
|
|
336
|
+
controller.close();
|
|
337
|
+
},
|
|
338
|
+
}),
|
|
339
|
+
}),
|
|
340
|
+
};
|
|
341
|
+
const model = new AISDKLanguageModel(mockModel);
|
|
342
|
+
const events = [];
|
|
343
|
+
for await (const event of model.stream({
|
|
344
|
+
input: [
|
|
345
|
+
{
|
|
346
|
+
kind: "message",
|
|
347
|
+
role: "user",
|
|
348
|
+
id: "msg-1",
|
|
349
|
+
content: [{ kind: "text", text: "Hi" }],
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
settings: {},
|
|
353
|
+
})) {
|
|
354
|
+
events.push(event);
|
|
355
|
+
}
|
|
356
|
+
// Check complete Message has metadata from end event
|
|
357
|
+
const messageEvent = events.find((e) => e.kind === "message");
|
|
358
|
+
expect(messageEvent).toMatchObject({
|
|
359
|
+
providerMetadata: { test: { version: "end" } }, // From end event, not start/delta
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
it("should pass through tool-call events unchanged", async () => {
|
|
363
|
+
const mockModel = {
|
|
364
|
+
provider: "test",
|
|
365
|
+
modelId: "test-model",
|
|
366
|
+
doStream: vi.fn().mockResolvedValue({
|
|
367
|
+
stream: new ReadableStream({
|
|
368
|
+
start(controller) {
|
|
369
|
+
const parts = [
|
|
370
|
+
{
|
|
371
|
+
type: "tool-call",
|
|
372
|
+
toolCallId: "call-123",
|
|
373
|
+
toolName: "calculator",
|
|
374
|
+
input: '{"expression":"2+2"}',
|
|
375
|
+
providerMetadata: undefined,
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
type: "finish",
|
|
379
|
+
finishReason: "tool-calls",
|
|
380
|
+
usage: {
|
|
381
|
+
inputTokens: 5,
|
|
382
|
+
outputTokens: 10,
|
|
383
|
+
totalTokens: 15,
|
|
384
|
+
},
|
|
385
|
+
providerMetadata: undefined,
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
for (const part of parts) {
|
|
389
|
+
controller.enqueue(part);
|
|
390
|
+
}
|
|
391
|
+
controller.close();
|
|
392
|
+
},
|
|
393
|
+
}),
|
|
394
|
+
}),
|
|
395
|
+
};
|
|
396
|
+
const model = new AISDKLanguageModel(mockModel);
|
|
397
|
+
const events = [];
|
|
398
|
+
for await (const event of model.stream({
|
|
399
|
+
input: [
|
|
400
|
+
{
|
|
401
|
+
kind: "message",
|
|
402
|
+
role: "user",
|
|
403
|
+
id: "msg-1",
|
|
404
|
+
content: [{ kind: "text", text: "Calculate 2+2" }],
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
settings: {},
|
|
408
|
+
})) {
|
|
409
|
+
events.push(event);
|
|
410
|
+
}
|
|
411
|
+
// Should have tool-call and finish
|
|
412
|
+
expect(events).toHaveLength(2);
|
|
413
|
+
expect(events[0]).toMatchObject({
|
|
414
|
+
kind: "tool-call",
|
|
415
|
+
callId: "call-123",
|
|
416
|
+
toolId: "calculator",
|
|
417
|
+
state: "completed",
|
|
418
|
+
arguments: '{"expression":"2+2"}',
|
|
419
|
+
});
|
|
420
|
+
expect(events[1]).toMatchObject({ kind: "finish" });
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
package/dist/convert/message.js
CHANGED
package/dist/convert/response.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/convert/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/convert/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAIlE;;GAEG;AACH,wBAAuB,aAAa,CAClC,MAAM,EAAE,cAAc,CAAC,yBAAyB,CAAC,GAChD,aAAa,CAAC,wBAAwB,CAAC,CAgBzC;AAED;;GAEG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAC7B,wBAAwB,GAAG,IAAI,EAC/B,yBAAyB,CAqJ1B,CAAC"}
|
package/dist/convert/stream.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { COMPLETED, FAILED } from "@kernl-sdk/protocol";
|
|
1
2
|
import { WARNING } from "./response";
|
|
2
3
|
/**
|
|
3
4
|
* Convert AI SDK stream to async iterable of kernl stream events.
|
|
@@ -90,8 +91,9 @@ export const STREAM_PART = {
|
|
|
90
91
|
case "tool-call":
|
|
91
92
|
return {
|
|
92
93
|
kind: "tool-call",
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
callId: part.toolCallId,
|
|
95
|
+
toolId: part.toolName,
|
|
96
|
+
state: COMPLETED,
|
|
95
97
|
arguments: part.input,
|
|
96
98
|
providerMetadata: part.providerMetadata,
|
|
97
99
|
};
|
|
@@ -101,7 +103,7 @@ export const STREAM_PART = {
|
|
|
101
103
|
kind: "tool-result",
|
|
102
104
|
callId: part.toolCallId,
|
|
103
105
|
toolId: part.toolName,
|
|
104
|
-
state: part.isError ?
|
|
106
|
+
state: part.isError ? FAILED : COMPLETED,
|
|
105
107
|
result: part.result,
|
|
106
108
|
error: part.isError ? String(part.result) : null,
|
|
107
109
|
providerMetadata: part.providerMetadata,
|
package/dist/index.d.ts
CHANGED