@kernl-sdk/ai 0.1.2 → 0.2.5
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/.turbo/turbo-check-types.log +4 -0
- package/CHANGELOG.md +78 -0
- package/LICENSE +1 -1
- package/dist/__tests__/integration.test.js +277 -26
- package/dist/__tests__/language-model.test.js +2 -1
- package/dist/convert/__tests__/message.test.js +27 -2
- package/dist/convert/__tests__/stream.test.js +31 -7
- package/dist/convert/__tests__/ui-message.test.d.ts +2 -0
- package/dist/convert/__tests__/ui-message.test.d.ts.map +1 -0
- package/dist/convert/__tests__/ui-message.test.js +1836 -0
- package/dist/convert/__tests__/ui-stream.test.d.ts +2 -0
- package/dist/convert/__tests__/ui-stream.test.d.ts.map +1 -0
- package/dist/convert/__tests__/ui-stream.test.js +452 -0
- package/dist/convert/message.d.ts +2 -1
- package/dist/convert/message.d.ts.map +1 -1
- package/dist/convert/message.js +16 -10
- package/dist/convert/response.d.ts +2 -1
- package/dist/convert/response.d.ts.map +1 -1
- package/dist/convert/response.js +66 -46
- package/dist/convert/settings.d.ts +2 -1
- package/dist/convert/settings.d.ts.map +1 -1
- package/dist/convert/stream.d.ts +2 -1
- package/dist/convert/stream.d.ts.map +1 -1
- package/dist/convert/stream.js +12 -17
- package/dist/convert/tools.d.ts +2 -1
- package/dist/convert/tools.d.ts.map +1 -1
- package/dist/convert/ui-message.d.ts +40 -0
- package/dist/convert/ui-message.d.ts.map +1 -0
- package/dist/convert/ui-message.js +324 -0
- package/dist/convert/ui-stream.d.ts +29 -0
- package/dist/convert/ui-stream.d.ts.map +1 -0
- package/dist/convert/ui-stream.js +139 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/language-model.d.ts.map +1 -1
- package/dist/language-model.js +72 -83
- package/package.json +11 -7
- package/src/__tests__/integration.test.ts +789 -507
- package/src/__tests__/language-model.test.ts +2 -1
- package/src/convert/__tests__/message.test.ts +29 -2
- package/src/convert/__tests__/stream.test.ts +34 -7
- package/src/convert/__tests__/ui-message.test.ts +2008 -0
- package/src/convert/__tests__/ui-stream.test.ts +547 -0
- package/src/convert/message.ts +18 -13
- package/src/convert/response.ts +82 -52
- package/src/convert/settings.ts +2 -1
- package/src/convert/stream.ts +22 -20
- package/src/convert/tools.ts +1 -1
- package/src/convert/ui-message.ts +409 -0
- package/src/convert/ui-stream.ts +167 -0
- package/src/index.ts +2 -0
- package/src/language-model.ts +78 -87
- package/tsconfig.json +1 -1
- package/vitest.config.ts +1 -0
- package/src/error.ts +0 -16
- package/src/types.ts +0 -0
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# @kernl/ai
|
|
2
2
|
|
|
3
|
+
## 0.2.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Migrate packages from GitHub Packages to npm registry.
|
|
8
|
+
|
|
9
|
+
**Breaking change for `kernl` (formerly `@kernl-sdk/core`):**
|
|
10
|
+
|
|
11
|
+
The core package has been renamed from `@kernl-sdk/core` to `kernl`. Update your imports:
|
|
12
|
+
|
|
13
|
+
```diff
|
|
14
|
+
- import { Agent, Kernl } from "@kernl-sdk/core";
|
|
15
|
+
+ import { Agent, Kernl } from "kernl";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
All other packages remain under the `@kernl-sdk` scope and are now publicly available on npm.
|
|
19
|
+
|
|
20
|
+
- Updated dependencies
|
|
21
|
+
- @kernl-sdk/shared@0.1.5
|
|
22
|
+
- @kernl-sdk/protocol@0.2.4
|
|
23
|
+
|
|
24
|
+
## 0.2.4
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 7946b16: Fix handling of tool calls with no required parameters. When AI providers (particularly Anthropic) send empty string arguments for tools with all optional parameters, convert to valid JSON "{}" to prevent parsing errors. Also fix tool-call state to use IN_PROGRESS instead of COMPLETED.
|
|
29
|
+
|
|
30
|
+
## 0.2.3
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- 8551086: Add historyToUIMessages function to convert thread history to AI SDK UIMessage format for useChat hook. Preserves providerMetadata on all parts (text, file, reasoning, tools) and groups tool calls with results.
|
|
35
|
+
|
|
36
|
+
## 0.2.2
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- Updated dependencies
|
|
41
|
+
- @kernl-sdk/shared@0.1.4
|
|
42
|
+
- @kernl-sdk/protocol@0.2.3
|
|
43
|
+
|
|
44
|
+
## 0.2.1
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- 05ce1f1: fix: handle tool result errors with error-text output
|
|
49
|
+
|
|
50
|
+
When a tool call fails and returns an error, the MESSAGE codec now properly encodes the error using the AI SDK's error-text output type instead of attempting to send null as a json value. This fixes the "Missing required parameter: 'output'" error that occurred when MCP tools returned errors.
|
|
51
|
+
|
|
52
|
+
## 0.2.0
|
|
53
|
+
|
|
54
|
+
### Minor Changes
|
|
55
|
+
|
|
56
|
+
- 0f25713: Add UI message and stream conversion utilities for AI SDK integration
|
|
57
|
+
- Add `UIMessageCodec` for bidirectional conversion between kernl and AI SDK message formats
|
|
58
|
+
- Add `toUIMessageStream()` helper to convert kernl streams to AI SDK UIMessageStream format
|
|
59
|
+
- Add `STREAM_UI_PART` codec for converting LanguageModelStreamEvent to UIMessageChunk
|
|
60
|
+
- Enable seamless integration with AI SDK's `useChat` hook and `createUIMessageStreamResponse`
|
|
61
|
+
- Add comprehensive test suites for both UI message and stream conversion
|
|
62
|
+
|
|
63
|
+
## 0.1.4
|
|
64
|
+
|
|
65
|
+
### Patch Changes
|
|
66
|
+
|
|
67
|
+
- 2c62c0a: Migrate from @kernl to @kernl-sdk scope
|
|
68
|
+
|
|
69
|
+
All packages have been migrated to the @kernl-sdk scope for publishing to GitHub Packages under the kernl-sdk organization.
|
|
70
|
+
|
|
71
|
+
- Updated dependencies [2c62c0a]
|
|
72
|
+
- @kernl-sdk/shared@0.1.3
|
|
73
|
+
- @kernl-sdk/protocol@0.2.2
|
|
74
|
+
|
|
75
|
+
## 0.1.3
|
|
76
|
+
|
|
77
|
+
### Patch Changes
|
|
78
|
+
|
|
79
|
+
- 19020a1: Fix tool call argument encoding for multi-turn conversations with Anthropic models
|
|
80
|
+
|
|
3
81
|
## 0.1.2
|
|
4
82
|
|
|
5
83
|
### Patch Changes
|
package/LICENSE
CHANGED
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright
|
|
189
|
+
Copyright 2025 Andrew Jones
|
|
190
190
|
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
192
|
you may not use this file except in compliance with the License.
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
2
|
import { openai } from "@ai-sdk/openai";
|
|
3
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
4
|
+
import { IN_PROGRESS } from "@kernl-sdk/protocol";
|
|
3
5
|
import { AISDKLanguageModel } from "../language-model";
|
|
4
6
|
/**
|
|
5
7
|
* Integration tests for AISDKLanguageModel with real AI SDK providers.
|
|
6
8
|
*
|
|
7
|
-
* These tests require
|
|
8
|
-
*
|
|
9
|
+
* These tests require API keys to be set:
|
|
10
|
+
* - OPENAI_API_KEY for OpenAI tests
|
|
11
|
+
* - ANTHROPIC_API_KEY for Anthropic tests
|
|
9
12
|
*
|
|
10
|
-
* Run with: OPENAI_API_KEY=your-key pnpm test:run
|
|
13
|
+
* Run with: OPENAI_API_KEY=your-key ANTHROPIC_API_KEY=your-key pnpm test:run
|
|
11
14
|
*/
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const SKIP_OPENAI_TESTS = !process.env.OPENAI_API_KEY;
|
|
16
|
+
const SKIP_ANTHROPIC_TESTS = !process.env.ANTHROPIC_API_KEY;
|
|
17
|
+
describe.skipIf(SKIP_OPENAI_TESTS)("AISDKLanguageModel - OpenAI", () => {
|
|
18
|
+
let gpt41;
|
|
15
19
|
beforeAll(() => {
|
|
16
|
-
|
|
20
|
+
gpt41 = new AISDKLanguageModel(openai("gpt-4.1"));
|
|
17
21
|
});
|
|
18
22
|
describe("generate", () => {
|
|
19
23
|
it("should generate a simple text response", async () => {
|
|
20
|
-
const response = await
|
|
24
|
+
const response = await gpt41.generate({
|
|
21
25
|
input: [
|
|
22
26
|
{
|
|
23
27
|
kind: "message",
|
|
@@ -44,7 +48,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
44
48
|
expect(messages.length).toBeGreaterThan(0);
|
|
45
49
|
});
|
|
46
50
|
it("should handle system messages", async () => {
|
|
47
|
-
const response = await
|
|
51
|
+
const response = await gpt41.generate({
|
|
48
52
|
input: [
|
|
49
53
|
{
|
|
50
54
|
kind: "message",
|
|
@@ -73,7 +77,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
73
77
|
expect(response.usage.totalTokens).toBeGreaterThan(0);
|
|
74
78
|
});
|
|
75
79
|
it("should handle multi-turn conversations", async () => {
|
|
76
|
-
const response = await
|
|
80
|
+
const response = await gpt41.generate({
|
|
77
81
|
input: [
|
|
78
82
|
{
|
|
79
83
|
kind: "message",
|
|
@@ -106,7 +110,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
106
110
|
expect(assistantMessages.length).toBeGreaterThan(0);
|
|
107
111
|
});
|
|
108
112
|
it("should respect temperature setting", async () => {
|
|
109
|
-
const response = await
|
|
113
|
+
const response = await gpt41.generate({
|
|
110
114
|
input: [
|
|
111
115
|
{
|
|
112
116
|
kind: "message",
|
|
@@ -124,7 +128,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
124
128
|
expect(response.usage.totalTokens).toBeGreaterThan(0);
|
|
125
129
|
});
|
|
126
130
|
it("should respect maxTokens setting", async () => {
|
|
127
|
-
const response = await
|
|
131
|
+
const response = await gpt41.generate({
|
|
128
132
|
input: [
|
|
129
133
|
{
|
|
130
134
|
kind: "message",
|
|
@@ -146,7 +150,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
146
150
|
describe("stream", () => {
|
|
147
151
|
it("should stream text responses", async () => {
|
|
148
152
|
const events = [];
|
|
149
|
-
for await (const event of
|
|
153
|
+
for await (const event of gpt41.stream({
|
|
150
154
|
input: [
|
|
151
155
|
{
|
|
152
156
|
kind: "message",
|
|
@@ -173,7 +177,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
173
177
|
});
|
|
174
178
|
it("should stream text deltas", async () => {
|
|
175
179
|
const events = [];
|
|
176
|
-
for await (const event of
|
|
180
|
+
for await (const event of gpt41.stream({
|
|
177
181
|
input: [
|
|
178
182
|
{
|
|
179
183
|
kind: "message",
|
|
@@ -200,7 +204,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
200
204
|
});
|
|
201
205
|
it("should handle limited token streams", async () => {
|
|
202
206
|
const events = [];
|
|
203
|
-
for await (const event of
|
|
207
|
+
for await (const event of gpt41.stream({
|
|
204
208
|
input: [
|
|
205
209
|
{
|
|
206
210
|
kind: "message",
|
|
@@ -223,7 +227,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
223
227
|
});
|
|
224
228
|
it("should yield both delta events and complete Message items", async () => {
|
|
225
229
|
const events = [];
|
|
226
|
-
for await (const event of
|
|
230
|
+
for await (const event of gpt41.stream({
|
|
227
231
|
input: [
|
|
228
232
|
{
|
|
229
233
|
kind: "message",
|
|
@@ -264,18 +268,75 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
264
268
|
expect(textContent.text).toBeDefined();
|
|
265
269
|
expect(textContent.text.length).toBeGreaterThan(0);
|
|
266
270
|
// Verify accumulated text matches concatenated deltas
|
|
267
|
-
const accumulatedFromDeltas = textDeltas
|
|
268
|
-
.map((d) => d.text)
|
|
269
|
-
.join("");
|
|
271
|
+
const accumulatedFromDeltas = textDeltas.map((d) => d.text).join("");
|
|
270
272
|
expect(textContent.text).toBe(accumulatedFromDeltas);
|
|
271
273
|
// Should have finish event
|
|
272
274
|
const finishEvents = events.filter((e) => e.kind === "finish");
|
|
273
275
|
expect(finishEvents.length).toBe(1);
|
|
274
276
|
});
|
|
277
|
+
it("should handle streaming tools with no required parameters (empty arguments)", async () => {
|
|
278
|
+
// Verify the empty arguments fix works in streaming mode as well
|
|
279
|
+
const events = [];
|
|
280
|
+
for await (const event of gpt41.stream({
|
|
281
|
+
input: [
|
|
282
|
+
{
|
|
283
|
+
kind: "message",
|
|
284
|
+
role: "user",
|
|
285
|
+
id: "msg-1",
|
|
286
|
+
content: [
|
|
287
|
+
{
|
|
288
|
+
kind: "text",
|
|
289
|
+
text: "Use the list_all_items tool",
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
tools: [
|
|
295
|
+
{
|
|
296
|
+
kind: "function",
|
|
297
|
+
name: "list_all_items",
|
|
298
|
+
description: "List all items",
|
|
299
|
+
parameters: {
|
|
300
|
+
type: "object",
|
|
301
|
+
properties: {
|
|
302
|
+
category: {
|
|
303
|
+
type: "string",
|
|
304
|
+
description: "Optional category filter",
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
// No required parameters
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
settings: {
|
|
312
|
+
maxTokens: 200,
|
|
313
|
+
temperature: 0,
|
|
314
|
+
toolChoice: { kind: "required" }, // Force tool use
|
|
315
|
+
},
|
|
316
|
+
})) {
|
|
317
|
+
events.push(event);
|
|
318
|
+
}
|
|
319
|
+
expect(events.length).toBeGreaterThan(0);
|
|
320
|
+
// Should have a tool-call event
|
|
321
|
+
const toolCalls = events.filter((e) => e.kind === "tool-call");
|
|
322
|
+
expect(toolCalls.length).toBeGreaterThan(0);
|
|
323
|
+
const toolCall = toolCalls[0];
|
|
324
|
+
expect(toolCall.callId).toBeDefined();
|
|
325
|
+
expect(toolCall.toolId).toBe("list_all_items");
|
|
326
|
+
expect(toolCall.state).toBe(IN_PROGRESS);
|
|
327
|
+
// Critical assertion: arguments should be valid JSON even if empty
|
|
328
|
+
expect(toolCall.arguments).toBeDefined();
|
|
329
|
+
expect(() => JSON.parse(toolCall.arguments)).not.toThrow();
|
|
330
|
+
const args = JSON.parse(toolCall.arguments);
|
|
331
|
+
expect(typeof args).toBe("object");
|
|
332
|
+
// Should have finish event
|
|
333
|
+
const finishEvents = events.filter((e) => e.kind === "finish");
|
|
334
|
+
expect(finishEvents.length).toBe(1);
|
|
335
|
+
});
|
|
275
336
|
});
|
|
276
337
|
describe("tools", () => {
|
|
277
338
|
it("should call tools when requested", async () => {
|
|
278
|
-
const response = await
|
|
339
|
+
const response = await gpt41.generate({
|
|
279
340
|
input: [
|
|
280
341
|
{
|
|
281
342
|
kind: "message",
|
|
@@ -317,7 +378,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
317
378
|
expect(toolCall.arguments).toBeDefined();
|
|
318
379
|
});
|
|
319
380
|
it("should handle tool choice setting", async () => {
|
|
320
|
-
const response = await
|
|
381
|
+
const response = await gpt41.generate({
|
|
321
382
|
input: [
|
|
322
383
|
{
|
|
323
384
|
kind: "message",
|
|
@@ -360,7 +421,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
360
421
|
expect(toolCalls.length).toBeGreaterThan(0);
|
|
361
422
|
});
|
|
362
423
|
it("should handle multiple tool calls", async () => {
|
|
363
|
-
const response = await
|
|
424
|
+
const response = await gpt41.generate({
|
|
364
425
|
input: [
|
|
365
426
|
{
|
|
366
427
|
kind: "message",
|
|
@@ -403,7 +464,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
403
464
|
});
|
|
404
465
|
it("should handle multi-turn conversation with tool results", async () => {
|
|
405
466
|
// First turn: get tool calls from the model
|
|
406
|
-
const firstResponse = await
|
|
467
|
+
const firstResponse = await gpt41.generate({
|
|
407
468
|
input: [
|
|
408
469
|
{
|
|
409
470
|
kind: "message",
|
|
@@ -442,7 +503,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
442
503
|
expect(toolCall.callId).toBeDefined();
|
|
443
504
|
expect(toolCall.toolId).toBe("calculate");
|
|
444
505
|
// Second turn: send tool results back to the model
|
|
445
|
-
const secondResponse = await
|
|
506
|
+
const secondResponse = await gpt41.generate({
|
|
446
507
|
input: [
|
|
447
508
|
{
|
|
448
509
|
kind: "message",
|
|
@@ -487,11 +548,73 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
487
548
|
const messages = secondResponse.content.filter((item) => item.kind === "message" && item.role === "assistant");
|
|
488
549
|
expect(messages.length).toBeGreaterThan(0);
|
|
489
550
|
});
|
|
551
|
+
it("should handle tools with no required parameters (empty arguments)", async () => {
|
|
552
|
+
// This test verifies the fix for empty string arguments
|
|
553
|
+
// When a tool has no required parameters and is called with no args,
|
|
554
|
+
// AI SDK sends input: "", which should be converted to "{}" for valid JSON
|
|
555
|
+
const response = await gpt41.generate({
|
|
556
|
+
input: [
|
|
557
|
+
{
|
|
558
|
+
kind: "message",
|
|
559
|
+
role: "user",
|
|
560
|
+
id: "msg-1",
|
|
561
|
+
content: [
|
|
562
|
+
{
|
|
563
|
+
kind: "text",
|
|
564
|
+
text: "Use the list_issues tool to get all issues",
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
tools: [
|
|
570
|
+
{
|
|
571
|
+
kind: "function",
|
|
572
|
+
name: "list_issues",
|
|
573
|
+
description: "List all issues in the system",
|
|
574
|
+
parameters: {
|
|
575
|
+
type: "object",
|
|
576
|
+
properties: {
|
|
577
|
+
status: {
|
|
578
|
+
type: "string",
|
|
579
|
+
description: "Optional status filter",
|
|
580
|
+
enum: ["open", "closed", "all"],
|
|
581
|
+
},
|
|
582
|
+
limit: {
|
|
583
|
+
type: "number",
|
|
584
|
+
description: "Optional limit on number of results",
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
// No required parameters - all are optional
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
settings: {
|
|
592
|
+
maxTokens: 200,
|
|
593
|
+
temperature: 0,
|
|
594
|
+
toolChoice: { kind: "required" }, // Force tool use
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
expect(response.content).toBeDefined();
|
|
598
|
+
// Should have a tool call
|
|
599
|
+
const toolCalls = response.content.filter((item) => item.kind === "tool-call");
|
|
600
|
+
expect(toolCalls.length).toBeGreaterThan(0);
|
|
601
|
+
const toolCall = toolCalls[0];
|
|
602
|
+
expect(toolCall.callId).toBeDefined();
|
|
603
|
+
expect(toolCall.toolId).toBe("list_issues");
|
|
604
|
+
expect(toolCall.state).toBe(IN_PROGRESS);
|
|
605
|
+
// The critical assertion: arguments should be valid JSON
|
|
606
|
+
// Even if the tool was called with no args, it should be "{}" not ""
|
|
607
|
+
expect(toolCall.arguments).toBeDefined();
|
|
608
|
+
expect(() => JSON.parse(toolCall.arguments)).not.toThrow();
|
|
609
|
+
// Parse should succeed and yield an object (possibly empty)
|
|
610
|
+
const args = JSON.parse(toolCall.arguments);
|
|
611
|
+
expect(typeof args).toBe("object");
|
|
612
|
+
});
|
|
490
613
|
});
|
|
491
614
|
describe("validation", () => {
|
|
492
615
|
it("should throw error for invalid maxTokens", async () => {
|
|
493
616
|
// AI SDK properly validates and throws errors for invalid values
|
|
494
|
-
await expect(
|
|
617
|
+
await expect(gpt41.generate({
|
|
495
618
|
input: [
|
|
496
619
|
{
|
|
497
620
|
kind: "message",
|
|
@@ -507,7 +630,7 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
507
630
|
});
|
|
508
631
|
it("should throw error for below minimum maxTokens", async () => {
|
|
509
632
|
// OpenAI requires minimum 16 tokens
|
|
510
|
-
await expect(
|
|
633
|
+
await expect(gpt41.generate({
|
|
511
634
|
input: [
|
|
512
635
|
{
|
|
513
636
|
kind: "message",
|
|
@@ -523,3 +646,131 @@ describe.skipIf(SKIP_INTEGRATION_TESTS)("AISDKLanguageModel integration", () =>
|
|
|
523
646
|
});
|
|
524
647
|
});
|
|
525
648
|
});
|
|
649
|
+
describe.skipIf(SKIP_ANTHROPIC_TESTS)("AISDKLanguageModel - Anthropic", () => {
|
|
650
|
+
let claude;
|
|
651
|
+
beforeAll(() => {
|
|
652
|
+
claude = new AISDKLanguageModel(anthropic("claude-sonnet-4-5"));
|
|
653
|
+
});
|
|
654
|
+
describe("tools", () => {
|
|
655
|
+
it("should handle tools with no required parameters (Anthropic-specific)", async () => {
|
|
656
|
+
// This test specifically verifies Anthropic's behavior with empty arguments
|
|
657
|
+
// Anthropic was the provider that sent input: "" for tools with no required params
|
|
658
|
+
const response = await claude.generate({
|
|
659
|
+
input: [
|
|
660
|
+
{
|
|
661
|
+
kind: "message",
|
|
662
|
+
role: "user",
|
|
663
|
+
id: "msg-1",
|
|
664
|
+
content: [
|
|
665
|
+
{
|
|
666
|
+
kind: "text",
|
|
667
|
+
text: "Use the list_issues tool",
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
},
|
|
671
|
+
],
|
|
672
|
+
tools: [
|
|
673
|
+
{
|
|
674
|
+
kind: "function",
|
|
675
|
+
name: "list_issues",
|
|
676
|
+
description: "List all issues in the system",
|
|
677
|
+
parameters: {
|
|
678
|
+
type: "object",
|
|
679
|
+
properties: {
|
|
680
|
+
status: {
|
|
681
|
+
type: "string",
|
|
682
|
+
description: "Optional status filter",
|
|
683
|
+
enum: ["open", "closed", "all"],
|
|
684
|
+
},
|
|
685
|
+
assignee: {
|
|
686
|
+
type: "string",
|
|
687
|
+
description: "Optional assignee filter",
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
// No required parameters - all are optional
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
],
|
|
694
|
+
settings: {
|
|
695
|
+
maxTokens: 200,
|
|
696
|
+
temperature: 0,
|
|
697
|
+
toolChoice: { kind: "required" }, // Force tool use
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
expect(response.content).toBeDefined();
|
|
701
|
+
// Should have a tool call
|
|
702
|
+
const toolCalls = response.content.filter((item) => item.kind === "tool-call");
|
|
703
|
+
expect(toolCalls.length).toBeGreaterThan(0);
|
|
704
|
+
const toolCall = toolCalls[0];
|
|
705
|
+
expect(toolCall.callId).toBeDefined();
|
|
706
|
+
expect(toolCall.toolId).toBe("list_issues");
|
|
707
|
+
expect(toolCall.state).toBe(IN_PROGRESS);
|
|
708
|
+
// Critical: Anthropic sends input: "" for tools with no required params
|
|
709
|
+
// Our adapter must convert this to "{}" for valid JSON
|
|
710
|
+
expect(toolCall.arguments).toBeDefined();
|
|
711
|
+
expect(() => JSON.parse(toolCall.arguments)).not.toThrow();
|
|
712
|
+
const args = JSON.parse(toolCall.arguments);
|
|
713
|
+
expect(typeof args).toBe("object");
|
|
714
|
+
});
|
|
715
|
+
it("should handle streaming tools with no required parameters (Anthropic-specific)", async () => {
|
|
716
|
+
// Verify the fix works in streaming mode with Anthropic
|
|
717
|
+
const events = [];
|
|
718
|
+
for await (const event of claude.stream({
|
|
719
|
+
input: [
|
|
720
|
+
{
|
|
721
|
+
kind: "message",
|
|
722
|
+
role: "user",
|
|
723
|
+
id: "msg-1",
|
|
724
|
+
content: [
|
|
725
|
+
{
|
|
726
|
+
kind: "text",
|
|
727
|
+
text: "Use the get_all_data tool",
|
|
728
|
+
},
|
|
729
|
+
],
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
tools: [
|
|
733
|
+
{
|
|
734
|
+
kind: "function",
|
|
735
|
+
name: "get_all_data",
|
|
736
|
+
description: "Get all data from the system",
|
|
737
|
+
parameters: {
|
|
738
|
+
type: "object",
|
|
739
|
+
properties: {
|
|
740
|
+
format: {
|
|
741
|
+
type: "string",
|
|
742
|
+
description: "Optional output format",
|
|
743
|
+
enum: ["json", "csv", "xml"],
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
// No required parameters
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
settings: {
|
|
751
|
+
maxTokens: 200,
|
|
752
|
+
temperature: 0,
|
|
753
|
+
toolChoice: { kind: "required" }, // Force tool use
|
|
754
|
+
},
|
|
755
|
+
})) {
|
|
756
|
+
events.push(event);
|
|
757
|
+
}
|
|
758
|
+
expect(events.length).toBeGreaterThan(0);
|
|
759
|
+
// Should have a tool-call event
|
|
760
|
+
const toolCalls = events.filter((e) => e.kind === "tool-call");
|
|
761
|
+
expect(toolCalls.length).toBeGreaterThan(0);
|
|
762
|
+
const toolCall = toolCalls[0];
|
|
763
|
+
expect(toolCall.callId).toBeDefined();
|
|
764
|
+
expect(toolCall.toolId).toBe("get_all_data");
|
|
765
|
+
expect(toolCall.state).toBe(IN_PROGRESS);
|
|
766
|
+
// Critical: arguments should be valid JSON even if Anthropic sent ""
|
|
767
|
+
expect(toolCall.arguments).toBeDefined();
|
|
768
|
+
expect(() => JSON.parse(toolCall.arguments)).not.toThrow();
|
|
769
|
+
const args = JSON.parse(toolCall.arguments);
|
|
770
|
+
expect(typeof args).toBe("object");
|
|
771
|
+
// Should have finish event
|
|
772
|
+
const finishEvents = events.filter((e) => e.kind === "finish");
|
|
773
|
+
expect(finishEvents.length).toBe(1);
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { IN_PROGRESS } from "@kernl-sdk/protocol";
|
|
2
3
|
import { AISDKLanguageModel } from "../language-model";
|
|
3
4
|
/**
|
|
4
5
|
* Unit tests for AISDKLanguageModel stream accumulation behavior
|
|
@@ -414,7 +415,7 @@ describe("AISDKLanguageModel", () => {
|
|
|
414
415
|
kind: "tool-call",
|
|
415
416
|
callId: "call-123",
|
|
416
417
|
toolId: "calculator",
|
|
417
|
-
state:
|
|
418
|
+
state: IN_PROGRESS,
|
|
418
419
|
arguments: '{"expression":"2+2"}',
|
|
419
420
|
});
|
|
420
421
|
expect(events[1]).toMatchObject({ kind: "finish" });
|
|
@@ -204,7 +204,7 @@ describe("MESSAGE codec", () => {
|
|
|
204
204
|
type: "tool-call",
|
|
205
205
|
toolCallId: "call-123",
|
|
206
206
|
toolName: "get_weather",
|
|
207
|
-
input:
|
|
207
|
+
input: { city: "SF" },
|
|
208
208
|
providerOptions: undefined,
|
|
209
209
|
},
|
|
210
210
|
],
|
|
@@ -251,6 +251,31 @@ describe("MESSAGE codec", () => {
|
|
|
251
251
|
],
|
|
252
252
|
});
|
|
253
253
|
});
|
|
254
|
+
it("should encode tool-result item with error", () => {
|
|
255
|
+
const result = MESSAGE.encode({
|
|
256
|
+
kind: "tool-result",
|
|
257
|
+
callId: "call-123",
|
|
258
|
+
toolId: "get_weather",
|
|
259
|
+
state: "failed",
|
|
260
|
+
result: null,
|
|
261
|
+
error: "Network timeout",
|
|
262
|
+
});
|
|
263
|
+
expect(result).toEqual({
|
|
264
|
+
role: "tool",
|
|
265
|
+
content: [
|
|
266
|
+
{
|
|
267
|
+
type: "tool-result",
|
|
268
|
+
toolCallId: "call-123",
|
|
269
|
+
toolName: "get_weather",
|
|
270
|
+
output: {
|
|
271
|
+
type: "error-text",
|
|
272
|
+
value: "Network timeout",
|
|
273
|
+
},
|
|
274
|
+
providerOptions: undefined,
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
});
|
|
278
|
+
});
|
|
254
279
|
});
|
|
255
280
|
describe("encode - reasoning items", () => {
|
|
256
281
|
it("should encode reasoning item with text", () => {
|
|
@@ -289,7 +314,7 @@ describe("MESSAGE codec", () => {
|
|
|
289
314
|
it("should throw error for unsupported item kind", () => {
|
|
290
315
|
expect(() => MESSAGE.encode({
|
|
291
316
|
kind: "unknown-kind",
|
|
292
|
-
})).toThrow("Unsupported LanguageModelItem kind
|
|
317
|
+
})).toThrow("Unsupported LanguageModelItem kind");
|
|
293
318
|
});
|
|
294
319
|
});
|
|
295
320
|
describe("decode", () => {
|