@kuralle-syrinx/aisdk 4.1.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,276 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
-
3
- import { describe, expect, it } from "vitest";
4
- import type { FinishReason, TextStreamPart, ToolSet } from "ai";
5
- import type { Reasoner, ReasonerTurn, ReasoningPart } from "@kuralle-syrinx/core";
6
- import { fromAiSdkAgent, fromStreamFactory, type AiSdkAgentLike } from "./from-ai-sdk.js";
7
-
8
- const ZERO_USAGE = {
9
- inputTokens: 0,
10
- inputTokenDetails: {
11
- noCacheTokens: 0,
12
- cacheReadTokens: 0,
13
- cacheWriteTokens: 0,
14
- },
15
- outputTokens: 0,
16
- outputTokenDetails: {
17
- textTokens: 0,
18
- reasoningTokens: 0,
19
- },
20
- totalTokens: 0,
21
- };
22
-
23
- function baseTurn(): ReasonerTurn {
24
- return {
25
- userText: "Hi",
26
- messages: [{ role: "system", content: "test" }],
27
- signal: new AbortController().signal,
28
- };
29
- }
30
-
31
- function textDelta(text: string): TextStreamPart<ToolSet> {
32
- return { type: "text-delta", id: "0", text, providerMetadata: undefined } as TextStreamPart<ToolSet>;
33
- }
34
-
35
- function finish(finishReason: FinishReason, rawFinishReason?: string): TextStreamPart<ToolSet> {
36
- return {
37
- type: "finish",
38
- finishReason,
39
- rawFinishReason,
40
- totalUsage: ZERO_USAGE,
41
- usage: ZERO_USAGE,
42
- providerMetadata: undefined,
43
- response: {},
44
- } as TextStreamPart<ToolSet>;
45
- }
46
-
47
- function finishStep(finishReason: FinishReason, rawFinishReason?: string): TextStreamPart<ToolSet> {
48
- return {
49
- type: "finish-step",
50
- finishReason,
51
- rawFinishReason,
52
- usage: ZERO_USAGE,
53
- providerMetadata: undefined,
54
- response: {},
55
- } as TextStreamPart<ToolSet>;
56
- }
57
-
58
- function toolCall(toolCallId: string, toolName: string, input: Record<string, unknown>): TextStreamPart<ToolSet> {
59
- return {
60
- type: "tool-call",
61
- toolCallId,
62
- toolName,
63
- input,
64
- } as TextStreamPart<ToolSet>;
65
- }
66
-
67
- function toolResult(
68
- toolCallId: string,
69
- toolName: string,
70
- output: unknown,
71
- ): TextStreamPart<ToolSet> {
72
- return {
73
- type: "tool-result",
74
- toolCallId,
75
- toolName,
76
- input: {},
77
- output,
78
- } as TextStreamPart<ToolSet>;
79
- }
80
-
81
- function errorPart(error: unknown): TextStreamPart<ToolSet> {
82
- return { type: "error", error } as TextStreamPart<ToolSet>;
83
- }
84
-
85
- function toolErrorPart(toolCallId: string, toolName: string, error: unknown): TextStreamPart<ToolSet> {
86
- return {
87
- type: "tool-error",
88
- toolCallId,
89
- toolName,
90
- input: {},
91
- error,
92
- } as TextStreamPart<ToolSet>;
93
- }
94
-
95
- async function collectParts(reasoner: Reasoner, turn: ReasonerTurn): Promise<ReasoningPart[]> {
96
- const parts: ReasoningPart[] = [];
97
- for await (const part of reasoner.stream(turn)) {
98
- parts.push(part);
99
- }
100
- return parts;
101
- }
102
-
103
- describe("from-ai-sdk adapters", () => {
104
- it("maps happy path: deltas, tool-call, tool-result, finish:stop", async () => {
105
- const reasoner = fromStreamFactory(async function* () {
106
- yield textDelta("Hello ");
107
- yield textDelta("world.");
108
- yield toolCall("tc-1", "get_weather", { city: "NYC" });
109
- yield toolResult("tc-1", "get_weather", { temp: 72 });
110
- yield finish("stop");
111
- });
112
-
113
- const parts = await collectParts(reasoner, baseTurn());
114
-
115
- expect(parts).toEqual([
116
- { type: "text-delta", text: "Hello " },
117
- { type: "text-delta", text: "world." },
118
- {
119
- type: "tool-call",
120
- toolId: "tc-1",
121
- toolName: "get_weather",
122
- args: { city: "NYC" },
123
- },
124
- {
125
- type: "tool-result",
126
- toolId: "tc-1",
127
- toolName: "get_weather",
128
- result: JSON.stringify({ temp: 72 }),
129
- },
130
- { type: "finish", reason: "stop", text: "Hello world." },
131
- ]);
132
- });
133
-
134
- it("maps error part to terminal error", async () => {
135
- const reasoner = fromStreamFactory(async function* () {
136
- yield textDelta("partial");
137
- yield errorPart(new Error("provider failed"));
138
- });
139
-
140
- const parts = await collectParts(reasoner, baseTurn());
141
-
142
- expect(parts).toHaveLength(2);
143
- expect(parts[0]).toEqual({ type: "text-delta", text: "partial" });
144
- expect(parts[1]?.type).toBe("error");
145
- if (parts[1]?.type === "error") {
146
- expect(parts[1].cause.message).toBe("provider failed");
147
- expect(parts[1].recoverable).toBe(false);
148
- }
149
- });
150
-
151
- it("maps tool-error part to terminal error", async () => {
152
- const reasoner = fromStreamFactory(async function* () {
153
- yield toolErrorPart("tc-1", "broken_tool", new Error("tool exploded"));
154
- });
155
-
156
- const parts = await collectParts(reasoner, baseTurn());
157
-
158
- expect(parts).toHaveLength(1);
159
- expect(parts[0]?.type).toBe("error");
160
- if (parts[0]?.type === "error") {
161
- expect(parts[0].cause.message).toBe("tool exploded");
162
- }
163
- });
164
-
165
- it("maps finish-step(error) to terminal error", async () => {
166
- const reasoner = fromStreamFactory(async function* () {
167
- yield finishStep("error", "MALFORMED_FUNCTION_CALL");
168
- });
169
-
170
- const parts = await collectParts(reasoner, baseTurn());
171
-
172
- expect(parts).toHaveLength(1);
173
- expect(parts[0]?.type).toBe("error");
174
- if (parts[0]?.type === "error") {
175
- expect(parts[0].cause.message).toBe(
176
- "AI SDK provider step failed: error (MALFORMED_FUNCTION_CALL)",
177
- );
178
- expect(parts[0].recoverable).toBe(true);
179
- }
180
- });
181
-
182
- it("maps finish(length) to finish with accumulated text", async () => {
183
- const reasoner = fromStreamFactory(async function* () {
184
- yield textDelta("truncated");
185
- yield finish("length", "MAX_TOKENS");
186
- });
187
-
188
- const parts = await collectParts(reasoner, baseTurn());
189
-
190
- expect(parts).toEqual([
191
- { type: "text-delta", text: "truncated" },
192
- { type: "finish", reason: "length", text: "truncated" },
193
- ]);
194
- });
195
-
196
- it("drops reasoning-delta and tool-input-start parts", async () => {
197
- const reasoner = fromStreamFactory(async function* () {
198
- yield {
199
- type: "reasoning-delta",
200
- id: "r1",
201
- text: "thinking...",
202
- providerMetadata: undefined,
203
- } as TextStreamPart<ToolSet>;
204
- yield {
205
- type: "tool-input-start",
206
- id: "t1",
207
- toolName: "search",
208
- providerMetadata: undefined,
209
- } as TextStreamPart<ToolSet>;
210
- yield textDelta("answer");
211
- yield finish("stop");
212
- });
213
-
214
- const parts = await collectParts(reasoner, baseTurn());
215
-
216
- expect(parts).toEqual([
217
- { type: "text-delta", text: "answer" },
218
- { type: "finish", reason: "stop", text: "answer" },
219
- ]);
220
- });
221
-
222
- it("yields first text-delta before source stream completes (no buffering)", async () => {
223
- let resolveGate: (() => void) | undefined;
224
- const gate = new Promise<void>((resolve) => {
225
- resolveGate = resolve;
226
- });
227
-
228
- const reasoner = fromStreamFactory(async function* () {
229
- yield textDelta("immediate");
230
- await gate;
231
- });
232
-
233
- const iterator = reasoner.stream(baseTurn())[Symbol.asyncIterator]();
234
- const first = await iterator.next();
235
-
236
- expect(first.done).toBe(false);
237
- expect(first.value).toEqual({ type: "text-delta", text: "immediate" });
238
- resolveGate?.();
239
- });
240
-
241
- it("maps stream ending without finish to terminal error", async () => {
242
- const reasoner = fromStreamFactory(async function* () {
243
- yield textDelta("no finish");
244
- });
245
-
246
- const parts = await collectParts(reasoner, baseTurn());
247
-
248
- expect(parts).toHaveLength(2);
249
- expect(parts[1]?.type).toBe("error");
250
- if (parts[1]?.type === "error") {
251
- expect(parts[1].cause.message).toBe("AI SDK stream ended without a provider finish reason");
252
- }
253
- });
254
-
255
- it("fromAiSdkAgent maps agent fullStream through the same table", async () => {
256
- const agent: AiSdkAgentLike = {
257
- async stream() {
258
- return {
259
- fullStream: (async function* () {
260
- yield textDelta("From ");
261
- yield textDelta("agent");
262
- yield finish("stop");
263
- })(),
264
- };
265
- },
266
- };
267
-
268
- const parts = await collectParts(fromAiSdkAgent(agent), baseTurn());
269
-
270
- expect(parts).toEqual([
271
- { type: "text-delta", text: "From " },
272
- { type: "text-delta", text: "agent" },
273
- { type: "finish", reason: "stop", text: "From agent" },
274
- ]);
275
- });
276
- });