ai-test-kit 0.0.1 → 1.0.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/README.md CHANGED
@@ -1,26 +1,21 @@
1
1
  # ai-test-kit
2
2
 
3
- <p align="center">Test utilities for the AI SDK: mock models, content and stream-part builders, fully type-safe</p>
3
+ <p align="center">Test Kit for AI SDK: mock models, content builders and stream helpers, fully type-safe</p>
4
4
  <p align="center">
5
5
  <a href="https://www.npmjs.com/package/ai-test-kit" alt="ai-test-kit"><img src="https://img.shields.io/npm/dt/ai-test-kit?label=ai-test-kit"></a> <a href="https://github.com/zirkelc/ai-test-kit/actions/workflows/ci.yml" alt="CI"><img src="https://img.shields.io/github/actions/workflow/status/zirkelc/ai-test-kit/ci.yml?branch=main"></a>
6
6
  </p>
7
7
 
8
- This library provides ergonomic, type-safe helpers for testing code built on the AI SDK: a fluent API to mock [`generateText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text) / [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text), build content and stream parts, and assert on results. It implements its own `LanguageModelV3` mock and builds on the AI SDK's own stream helpers (`simulateReadableStream` and the stream converters).
8
+ This library provides a simple, type-safe API for testing AI SDK-powered apps. It gives you small, composable builders for mocking models, generating content and stream parts, and asserting on the results, so your tests stay short, deterministic, and fully typed.
9
9
 
10
10
  ### Why?
11
11
 
12
- The AI SDK ships `MockLanguageModelV3` and other helpers under `ai/test`, but they are deliberately low-level. In practice every project ends up rebuilding the same helpers to:
12
+ The AI SDK ships `MockLanguageModelV3` and a few other test primitives, but they are deliberately low-level. In practice every project ends up rebuilding the same helpers to:
13
13
 
14
14
  - **Mock a model**: return text, throw an error, or replay a scripted response per call
15
- - **Build content and stream parts**: assemble valid `text-start` → `text-delta` → `text-end` → `finish` streams by hand
15
+ - **Generate content and stream parts**: assemble valid `text-start` → `text-delta` → `text-end` → `finish` streams by hand
16
16
  - **Keep tests deterministic**: pin message ids and timestamps so snapshots are stable
17
17
 
18
- This library provides those helpers as small, composable builders. Models are `vi.fn()` spies, so you can assert on calls with the full Vitest API while also reading the recorded call arguments directly.
19
-
20
- Helpers are split by layer, each under its own entry point so an import only pulls in the types it needs:
21
-
22
- - `ai-test-kit/language` — the model layer: mock a `LanguageModelV3`, build `LanguageModelV3Content` and stream parts (and later `ai-test-kit/embedding`, `ai-test-kit/image`)
23
- - `ai-test-kit/ui` — the UI layer: build `UIMessagePart`, `UIMessageChunk`, and `UIMessage` fixtures, optionally typed to your own `UIMessage`
18
+ This library ships those helpers, ready to use. Models are `vi.fn()` spies, so you can assert on calls with the full Vitest API while also reading the recorded call arguments directly.
24
19
 
25
20
  ### Installation
26
21
 
@@ -32,6 +27,11 @@ npm install -D ai-test-kit
32
27
 
33
28
  ## Usage
34
29
 
30
+ The API is split by layer, each under its own entry point so an import only pulls in the types it needs:
31
+
32
+ - `ai-test-kit/language` — the model layer: mock a `LanguageModelV3`, generate `LanguageModelV3Content` for [`generateText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text) and stream parts `LanguageModelV3StreamPart[]` for [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text)
33
+ - `ai-test-kit/ui` — the UI layer: build `UIMessagePart`, `UIMessageChunk`, and `UIMessage` fixtures, optionally typed to your own `UIMessage`
34
+
35
35
  ### Language Models
36
36
 
37
37
  Helpers from `ai-test-kit/language` to mock a model and build the content and stream parts it returns.
@@ -108,14 +108,14 @@ result.toolCalls[0].toolName; // 'weather'
108
108
 
109
109
  #### Usage and Finish Reason
110
110
 
111
- By default a mock reports a `stop` finish reason and a small fixed token usage. Override them via the `{ content, finishReason, usage }` form to test code that branches on the finish reason or tracks token usage.
111
+ By default a mock reports a `stop` finish reason and a small fixed token usage. Override them via the `{ content, finishReason, usage }` form to test code that branches on the finish reason or tracks token usage. `finishReason` accepts a bare unified string (`'length'`) or a full object via `MockLanguageModel.finishReason(...)`.
112
112
 
113
113
  ```typescript
114
114
  import { Content, MockLanguageModel } from 'ai-test-kit/language';
115
115
 
116
116
  const model = MockLanguageModel.from({
117
117
  content: [Content.text('truncated…')],
118
- finishReason: MockLanguageModel.finishReason('length'),
118
+ finishReason: 'length',
119
119
  usage: MockLanguageModel.usage({ outputTokens: { total: 50 } }),
120
120
  });
121
121
 
@@ -154,6 +154,23 @@ const model = MockLanguageModel.from({
154
154
  });
155
155
  ```
156
156
 
157
+ #### Aborting a Stream
158
+
159
+ The call's `abortSignal` is wired into the simulated stream automatically, so a stream aborted mid-flight errors with an `AbortError` just like a real provider — no custom `ReadableStream` needed. Pair it with `chunkDelayInMs` so the abort can land between chunks.
160
+
161
+ ```typescript
162
+ import { MockLanguageModel, StreamParts } from 'ai-test-kit/language';
163
+
164
+ const controller = new AbortController();
165
+
166
+ const model = MockLanguageModel.from({
167
+ stream: { chunks: [...StreamParts.text('Hello World'), StreamParts.finish()], chunkDelayInMs: 10 },
168
+ });
169
+
170
+ const result = streamText({ model, prompt: 'Hi', abortSignal: controller.signal });
171
+ // ...later, controller.abort() makes the stream reject with an AbortError
172
+ ```
173
+
157
174
  #### Different Responses per Method
158
175
 
159
176
  Use the `{ generate, stream }` form to drive `doGenerate` and `doStream` independently — for example to return plain text non-streaming but a richer sequence when streamed.
@@ -167,6 +184,18 @@ const model = MockLanguageModel.from({
167
184
  });
168
185
  ```
169
186
 
187
+ #### Input-Dependent Responses
188
+
189
+ For a response that depends on the call (the prompt, tools, or settings), pass a function to `generate` / `stream`. It receives the `doGenerate` / `doStream` call options and returns the result directly — the escape hatch for cases the declarative forms can't express, including a fully custom `LanguageModelV3StreamResult`. The result builders pair well here.
190
+
191
+ ```typescript
192
+ import { MockLanguageModel } from 'ai-test-kit/language';
193
+
194
+ const model = MockLanguageModel.from({
195
+ generate: async ({ prompt }) => MockLanguageModel.generateResult(prompt.length > 1 ? 'multi-turn' : 'single-turn'),
196
+ });
197
+ ```
198
+
170
199
  #### Inspecting Streams
171
200
 
172
201
  Use `Stream` to build, drain, and read stream parts when asserting.
@@ -307,6 +336,7 @@ MockLanguageModel.from(input?: MockResponse | MockResponse[], options?: MockLang
307
336
  // MockLanguageModel.from(new Error('429')): a model that throws from generate and stream
308
337
  // MockLanguageModel.from({ content: [Content.text('Hi')] }): a model returning those parts (stream derived from them)
309
338
  // MockLanguageModel.from({ generate: 'A', stream: [...] }): drives doGenerate and doStream independently
339
+ // MockLanguageModel.from({ generate: (options) => result }): a function of the call options (input-dependent)
310
340
  // MockLanguageModel.from([new Error('429'), 'ok']): sequences responses per call, clamping to the last
311
341
  ```
312
342
 
@@ -332,9 +362,10 @@ MockLanguageModel.generateResult(input: string | { content: LanguageModelV3Conte
332
362
  #### `.streamResult(input, options?)`
333
363
 
334
364
  ```ts
335
- MockLanguageModel.streamResult(input: string | LanguageModelV3StreamPart[], options?: StreamDelayOptions): LanguageModelV3StreamResult
365
+ MockLanguageModel.streamResult(input: string | LanguageModelV3StreamPart[] | ReadableStream<LanguageModelV3StreamPart>, options?: StreamDelayOptions): LanguageModelV3StreamResult
336
366
  // MockLanguageModel.streamResult('hi'): { stream } — a ReadableStream of stream-start → text → finish
337
367
  // MockLanguageModel.streamResult([...StreamParts.text('hi'), StreamParts.finish()]): { stream } — a ReadableStream of the given parts
368
+ // MockLanguageModel.streamResult(Stream.from(parts)): { stream } — wraps an existing ReadableStream as-is (delays ignored)
338
369
  ```
339
370
 
340
371
  #### `.usage(overrides?)`
@@ -510,6 +541,13 @@ Stream.toArray<T>(stream: ReadableStream<T>): Promise<T[]>
510
541
  // Stream.toArray(stream): Promise<[a, b]>
511
542
  ```
512
543
 
544
+ #### `.toIterable(stream)`
545
+
546
+ ```ts
547
+ Stream.toIterable<T>(stream: ReadableStream<T>): ReadableStream<T> & AsyncIterable<T>
548
+ // for await (const part of Stream.toIterable(stream)) { ... } — consume a stream via for-await
549
+ ```
550
+
513
551
  #### `.text(parts)`
514
552
 
515
553
  ```ts
@@ -524,6 +562,31 @@ Stream.finishReason(parts: LanguageModelV3StreamPart[]): LanguageModelV3FinishRe
524
562
  // Stream.finishReason([StreamParts.finish()]): { unified: 'stop', raw: 'stop' }
525
563
  ```
526
564
 
565
+ #### `Iterable`
566
+
567
+ The async-iterable complement to `Stream`: build, drain, and convert `AsyncIterable`s (async generators, and anything consumed via `for await`). Use it when the code under test produces or consumes a plain async iterable rather than a `ReadableStream`. Cross back to the `Stream` toolbox with `.toStream`.
568
+
569
+ #### `.from(items)`
570
+
571
+ ```ts
572
+ Iterable.from<T>(items: T[]): AsyncIterable<T>
573
+ // Iterable.from([a, b]): an async iterable yielding a, then b
574
+ ```
575
+
576
+ #### `.toArray(iterable)`
577
+
578
+ ```ts
579
+ Iterable.toArray<T>(iterable: AsyncIterable<T>): Promise<T[]>
580
+ // Iterable.toArray(iterable): Promise<[a, b]>
581
+ ```
582
+
583
+ #### `.toStream(iterable)`
584
+
585
+ ```ts
586
+ Iterable.toStream<T>(iterable: AsyncIterable<T>): ReadableStream<T>
587
+ // Iterable.toStream(iterable): a ReadableStream emitting a, then b
588
+ ```
589
+
527
590
  #### `Options`
528
591
 
529
592
  Determinism helpers to spread into `generateText()` / `streamText()`.
@@ -886,7 +949,7 @@ import type { MockLanguageModel } from 'ai-test-kit/language';
886
949
 
887
950
  ### `GenerateResponse` / `StreamResponse`
888
951
 
889
- The per-method response shapes used by the `{ generate, stream }` form of `MockResponse`. `stream` accepts a bare `Array<StreamPart>`, or a `{ chunks, initialDelayInMs?, chunkDelayInMs? }` object to simulate delays.
952
+ The per-method response shapes used by the `{ generate, stream }` form of `MockResponse`. `stream` accepts a bare `Array<StreamPart>`, a `ReadableStream<StreamPart>` (used as-is), or a `{ chunks, initialDelayInMs?, chunkDelayInMs? }` object to simulate delays. Both also accept a `string`, an `Error`, or a **function** of the call options returning the result directly (the escape hatch for input-dependent responses).
890
953
 
891
954
  ```ts
892
955
  import type { GenerateResponse, StreamResponse } from 'ai-test-kit/language';
@@ -912,11 +975,11 @@ import type { StreamPartOptions } from 'ai-test-kit/language';
912
975
 
913
976
  ### `StreamDelayOptions`
914
977
 
915
- Simulated timing shared by `Stream.simulate`, `MockLanguageModel.streamResult`, and the `stream` chunks form.
978
+ Simulated timing shared by `Stream.simulate`, `MockLanguageModel.streamResult`, and the `stream` chunks form. With an `abortSignal`, the stream errors with an `AbortError` the instant the signal fires (mid-delay), matching a real provider stream.
916
979
 
917
980
  ```ts
918
981
  import type { StreamDelayOptions } from 'ai-test-kit/language';
919
- // { initialDelayInMs?: number; chunkDelayInMs?: number }
982
+ // { initialDelayInMs?: number | null; chunkDelayInMs?: number | null; abortSignal?: AbortSignal }
920
983
  ```
921
984
 
922
985
  ## License
@@ -4,29 +4,40 @@ import { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3Content, La
4
4
  //#region src/language/stream.d.ts
5
5
  /** Simulated timing for a stream. Shared by `Stream.simulate`, `MockLanguageModel.streamResult`, and the `stream` chunks form. */
6
6
  type StreamDelayOptions = {
7
- /** Delay before the first part is emitted. */initialDelayInMs?: number; /** Delay between each subsequent part. */
8
- chunkDelayInMs?: number;
7
+ /** Delay before the first part is emitted; `null` skips the delay. Defaults to `0`. */initialDelayInMs?: number | null; /** Delay between each subsequent part; `null` skips the delay. Defaults to `0`. */
8
+ chunkDelayInMs?: number | null; /** When provided, the stream errors with an `AbortError` the instant the signal fires. */
9
+ abortSignal?: AbortSignal;
9
10
  };
10
11
  /** Operations for building, draining, and inspecting language model streams in tests. */
11
12
  declare const Stream: {
12
13
  /** Builds a `ReadableStream` from an array of parts. */from: <PART>(parts: Array<PART>) => ReadableStream<PART>; /** Builds a `ReadableStream` that emits parts with optional delays, for timing tests. */
13
14
  simulate: <PART>(chunks: Array<PART>, opts?: StreamDelayOptions) => ReadableStream<PART>; /** Reads a stream to completion and returns every part it emitted. */
14
- toArray: <PART>(stream: ReadableStream<PART>) => Promise<Array<PART>>; /** Joins the `text-delta` parts of a stream-part sequence into the full text. */
15
+ toArray: <PART>(stream: ReadableStream<PART>) => Promise<Array<PART>>; /** Wraps a `ReadableStream` so it can also be consumed via `for await`. */
16
+ toIterable: <PART>(stream: ReadableStream<PART>) => ReadableStream<PART> & AsyncIterable<PART>; /** Joins the `text-delta` parts of a stream-part sequence into the full text. */
15
17
  text: (parts: Array<LanguageModelV3StreamPart>) => string; /** Returns the finish reason from a stream-part sequence, if a `finish` part is present. */
16
18
  finishReason: (parts: Array<LanguageModelV3StreamPart>) => LanguageModelV3FinishReason | undefined;
17
19
  };
18
20
  //#endregion
19
21
  //#region src/language/mock-language-model.d.ts
20
22
  /** A (possibly partial) non-streaming result; only `content` is required, the rest defaults. */
21
- type GenerateResultInput = Partial<LanguageModelV3GenerateResult> & {
22
- content: Array<LanguageModelV3Content>;
23
+ type GenerateResultInput = Omit<Partial<LanguageModelV3GenerateResult>, 'finishReason'> & {
24
+ content: Array<LanguageModelV3Content>; /** The finish reason, as a full object or a bare unified value (e.g. `'length'`). */
25
+ finishReason?: LanguageModelV3FinishReason | LanguageModelV3FinishReason['unified'];
23
26
  };
24
- /** How to respond to a `doGenerate` call. */
27
+ /**
28
+ * How to respond to a `doGenerate` call. A function receives the call options and returns the generate
29
+ * result directly — the escape hatch for input-dependent responses.
30
+ */
25
31
  type GenerateResponse = string | Error | GenerateResultInput | LanguageModelV3['doGenerate'];
26
- /** How to respond to a `doStream` call. A bare array streams without delay; the object form adds delays. */
27
- type StreamResponse = string | Error | Array<LanguageModelV3StreamPart> | ({
32
+ /**
33
+ * How to respond to a `doStream` call. A bare array (or `ReadableStream`) streams without delay; the
34
+ * `{ chunks, ... }` form adds delays and abort handling. A function receives the call options and
35
+ * returns the stream result directly — the escape hatch for input-dependent streams or a fully custom
36
+ * `LanguageModelV3StreamResult` (e.g. one carrying response metadata).
37
+ */
38
+ type StreamResponse = string | Error | Array<LanguageModelV3StreamPart> | ReadableStream<LanguageModelV3StreamPart> | ({
28
39
  chunks: Array<LanguageModelV3StreamPart>;
29
- } & StreamDelayOptions) | LanguageModelV3StreamResult | LanguageModelV3['doStream'];
40
+ } & StreamDelayOptions) | LanguageModelV3['doStream'];
30
41
  /**
31
42
  * A single mock response. A `string` or `Error` applies to whichever method is called;
32
43
  * the object forms target one method explicitly.
@@ -84,7 +95,7 @@ declare const MockLanguageModel: {
84
95
  from: (input?: MockResponse | Array<MockResponse>, options?: MockLanguageModelOptions) => LanguageModelMock;
85
96
  content: (input: string | Array<LanguageModelV3Content>) => Array<LanguageModelV3Content>;
86
97
  generateResult: (input: string | GenerateResultInput) => LanguageModelV3GenerateResult;
87
- streamResult: (input: string | Array<LanguageModelV3StreamPart>, opts?: StreamDelayOptions) => LanguageModelV3StreamResult;
98
+ streamResult: (input: string | Array<LanguageModelV3StreamPart> | ReadableStream<LanguageModelV3StreamPart>, opts?: StreamDelayOptions) => LanguageModelV3StreamResult;
88
99
  usage: (overrides?: {
89
100
  inputTokens?: Partial<LanguageModelV3Usage["inputTokens"]>;
90
101
  outputTokens?: Partial<LanguageModelV3Usage["outputTokens"]>;
@@ -188,6 +199,20 @@ declare const StreamParts: {
188
199
  raw: (rawValue: unknown) => LanguageModelV3StreamPart;
189
200
  };
190
201
  //#endregion
202
+ //#region src/language/iterable.d.ts
203
+ /**
204
+ * Operations for building, draining, and converting async iterables in tests.
205
+ *
206
+ * The complement to `Stream`: where `Stream` works with `ReadableStream`s, `Iterable` works with
207
+ * `AsyncIterable`s (async generators and anything consumed via `for await`). Cross over to the full
208
+ * `Stream` toolbox with `Iterable.toStream`.
209
+ */
210
+ declare const Iterable: {
211
+ /** Builds an `AsyncIterable` that yields each item in order, e.g. to feed code that consumes one. */from: <ITEM>(items: Array<ITEM>) => AsyncIterable<ITEM>; /** Reads an async iterable to completion and returns every item it yielded. */
212
+ toArray: <ITEM>(iterable: AsyncIterable<ITEM>) => Promise<Array<ITEM>>; /** Converts an async iterable into a `ReadableStream`, e.g. to feed a `ReadableStream`-consuming API. */
213
+ toStream: <ITEM>(iterable: AsyncIterable<ITEM>) => ReadableStream<ITEM>;
214
+ };
215
+ //#endregion
191
216
  //#region src/language/options.d.ts
192
217
  /**
193
218
  * Determinism helpers to spread into `generateText`/`streamText` so ids and timestamps are stable.
@@ -215,4 +240,4 @@ declare const Options: {
215
240
  };
216
241
  };
217
242
  //#endregion
218
- export { Content, type GenerateResponse, MockLanguageModel, type MockLanguageModelOptions, type MockResponse, Options, Stream, type StreamDelayOptions, type StreamPartOptions, StreamParts, type StreamResponse };
243
+ export { Content, type GenerateResponse, Iterable, MockLanguageModel, type MockLanguageModelOptions, type MockResponse, Options, Stream, type StreamDelayOptions, type StreamPartOptions, StreamParts, type StreamResponse };
@@ -1,5 +1,4 @@
1
- import { t as tokenize } from "../tokenize-C-Zp26iY.mjs";
2
- import { simulateReadableStream } from "ai";
1
+ import { n as toJSONString, t as tokenize } from "../tokenize-Cy46iVSX.mjs";
3
2
  import { vi } from "vitest";
4
3
  import { convertArrayToReadableStream, convertReadableStreamToArray } from "@ai-sdk/provider-utils/test";
5
4
 
@@ -29,11 +28,6 @@ const toFinishReason = (reason) => typeof reason === "string" ? {
29
28
  raw: reason
30
29
  } : reason;
31
30
 
32
- //#endregion
33
- //#region src/internal/json.ts
34
- /** Stringifies tool input to the JSON string the provider spec expects, leaving strings untouched. */
35
- const toJSONString = (input) => typeof input === "string" ? input : JSON.stringify(input);
36
-
37
31
  //#endregion
38
32
  //#region src/language/content.ts
39
33
  /** Builders for the static content parts a language model returns from `doGenerate`. */
@@ -75,14 +69,108 @@ const Content = {
75
69
 
76
70
  //#endregion
77
71
  //#region src/language/stream.ts
72
+ /** The error a real provider stream rejects with when its request is aborted. */
73
+ const abortError = () => new DOMException("The user aborted a request.", "AbortError");
74
+ /** Waits `ms` (`null` resolves at once), resolving early if the signal aborts so the caller can react immediately. */
75
+ const delay = (ms, signal) => new Promise((resolve) => {
76
+ if (ms == null) {
77
+ resolve();
78
+ return;
79
+ }
80
+ const onAbort = () => {
81
+ clearTimeout(timer);
82
+ resolve();
83
+ };
84
+ const timer = setTimeout(() => {
85
+ signal?.removeEventListener("abort", onAbort);
86
+ resolve();
87
+ }, ms);
88
+ signal?.addEventListener("abort", onAbort, { once: true });
89
+ });
90
+ /**
91
+ * Builds a delayed `ReadableStream`, a port of the AI SDK's `simulateReadableStream` (delay before each
92
+ * part, `null` to skip) extended with abort handling: when an `abortSignal` fires it errors with an
93
+ * `AbortError` at once (even mid-delay), matching a real provider stream. Inert without a signal.
94
+ */
95
+ const simulateStream = (chunks, opts = {}) => {
96
+ const { abortSignal, initialDelayInMs = 0, chunkDelayInMs = 0 } = opts;
97
+ let index = 0;
98
+ return new ReadableStream({ async pull(controller) {
99
+ if (abortSignal?.aborted) {
100
+ controller.error(abortError());
101
+ return;
102
+ }
103
+ if (index >= chunks.length) {
104
+ controller.close();
105
+ return;
106
+ }
107
+ await delay(index === 0 ? initialDelayInMs : chunkDelayInMs, abortSignal);
108
+ if (abortSignal?.aborted) {
109
+ controller.error(abortError());
110
+ return;
111
+ }
112
+ controller.enqueue(chunks[index]);
113
+ index += 1;
114
+ } });
115
+ };
116
+ /**
117
+ * Wraps a `ReadableStream` so it can also be consumed via `for await`, piping through a fresh
118
+ * `TransformStream` so the source stays unlocked. Ported from the AI SDK's `createAsyncIterableStream`.
119
+ */
120
+ const streamToAsyncIterable = (source) => {
121
+ const stream = source.pipeThrough(new TransformStream());
122
+ return Object.assign(stream, { [Symbol.asyncIterator]() {
123
+ const reader = stream.getReader();
124
+ let finished = false;
125
+ const cleanup = async () => {
126
+ finished = true;
127
+ try {
128
+ await reader.cancel();
129
+ } finally {
130
+ try {
131
+ reader.releaseLock();
132
+ } catch {}
133
+ }
134
+ };
135
+ return {
136
+ async next() {
137
+ if (finished) return {
138
+ done: true,
139
+ value: void 0
140
+ };
141
+ const { done, value } = await reader.read();
142
+ if (done) {
143
+ await cleanup();
144
+ return {
145
+ done: true,
146
+ value: void 0
147
+ };
148
+ }
149
+ return {
150
+ done: false,
151
+ value
152
+ };
153
+ },
154
+ async return() {
155
+ await cleanup();
156
+ return {
157
+ done: true,
158
+ value: void 0
159
+ };
160
+ },
161
+ async throw(error) {
162
+ await cleanup();
163
+ throw error;
164
+ }
165
+ };
166
+ } });
167
+ };
78
168
  /** Operations for building, draining, and inspecting language model streams in tests. */
79
169
  const Stream = {
80
170
  from: (parts) => convertArrayToReadableStream(parts),
81
- simulate: (chunks, opts = {}) => simulateReadableStream({
82
- chunks,
83
- ...opts
84
- }),
171
+ simulate: (chunks, opts = {}) => simulateStream(chunks, opts),
85
172
  toArray: (stream) => convertReadableStreamToArray(stream),
173
+ toIterable: (stream) => streamToAsyncIterable(stream),
86
174
  text: (parts) => parts.filter((part) => part.type === "text-delta").map((part) => part.delta).join(""),
87
175
  finishReason: (parts) => parts.find((part) => part.type === "finish")?.finishReason
88
176
  };
@@ -206,19 +294,18 @@ const contentToStream = (content, finishReason, usage) => [
206
294
  ];
207
295
  /** The streamed form of a string is the streamed form of a single text content part. */
208
296
  const textToStream = (text) => contentToStream([Content.text(text)]);
209
- /** Fills a partial generate result with default finish reason, usage, and warnings. */
210
- const buildGenerateResult = (input) => ({
211
- finishReason: defaultFinishReason,
212
- usage: defaultUsage,
213
- warnings: [],
214
- ...input
215
- });
216
- /** Wraps stream parts into a stream result, with optional simulated delays. */
217
- const buildStreamResult = (chunks, initialDelayInMs, chunkDelayInMs) => ({ stream: simulateReadableStream({
218
- chunks,
219
- initialDelayInMs,
220
- chunkDelayInMs
221
- }) });
297
+ /** Fills a partial generate result with default finish reason, usage, and warnings; coerces a string finish reason. */
298
+ const buildGenerateResult = (input) => {
299
+ const { finishReason, ...rest } = input;
300
+ return {
301
+ finishReason: finishReason === void 0 ? defaultFinishReason : toFinishReason(finishReason),
302
+ usage: defaultUsage,
303
+ warnings: [],
304
+ ...rest
305
+ };
306
+ };
307
+ /** Wraps stream parts into a stream result, with optional simulated delays and abort handling. */
308
+ const buildStreamResult = (chunks, opts = {}) => ({ stream: simulateStream(chunks, opts) });
222
309
  /** Resolves the `generate` form of an explicit response into a generate result. */
223
310
  const resolveGenerateResponse = async (response, options) => {
224
311
  if (typeof response === "string") return buildGenerateResult({ content: [Content.text(response)] });
@@ -228,12 +315,17 @@ const resolveGenerateResponse = async (response, options) => {
228
315
  };
229
316
  /** Resolves the `stream` form of an explicit response into a stream result. */
230
317
  const resolveStreamResponse = async (response, options) => {
231
- if (typeof response === "string") return buildStreamResult(textToStream(response));
318
+ const { abortSignal } = options;
319
+ if (typeof response === "string") return buildStreamResult(textToStream(response), { abortSignal });
232
320
  if (response instanceof Error) throw response;
233
- if (Array.isArray(response)) return buildStreamResult(response);
321
+ if (Array.isArray(response)) return buildStreamResult(response, { abortSignal });
322
+ if (response instanceof ReadableStream) return { stream: response };
234
323
  if (typeof response === "function") return response(options);
235
- if ("chunks" in response) return buildStreamResult(response.chunks, response.initialDelayInMs, response.chunkDelayInMs);
236
- return response;
324
+ return buildStreamResult(response.chunks, {
325
+ initialDelayInMs: response.initialDelayInMs,
326
+ chunkDelayInMs: response.chunkDelayInMs,
327
+ abortSignal: response.abortSignal ?? abortSignal
328
+ });
237
329
  };
238
330
  /** Resolves a top-level response for a `doGenerate` call. */
239
331
  const resolveGenerate = async (response, options) => {
@@ -245,10 +337,11 @@ const resolveGenerate = async (response, options) => {
245
337
  };
246
338
  /** Resolves a top-level response for a `doStream` call. */
247
339
  const resolveStream = async (response, options) => {
248
- if (typeof response === "string") return buildStreamResult(textToStream(response));
340
+ const { abortSignal } = options;
341
+ if (typeof response === "string") return buildStreamResult(textToStream(response), { abortSignal });
249
342
  if (response instanceof Error) throw response;
250
343
  if (isExplicit(response)) return response.stream === void 0 ? notImplemented("doStream") : resolveStreamResponse(response.stream, options);
251
- if ("content" in response) return buildStreamResult(contentToStream(response.content, response.finishReason, response.usage));
344
+ if ("content" in response) return buildStreamResult(contentToStream(response.content, response.finishReason, response.usage), { abortSignal });
252
345
  return notImplemented("doStream");
253
346
  };
254
347
  /** Picks the response for the current call: a single response repeats, an array advances and clamps. */
@@ -300,7 +393,10 @@ const content = (input) => typeof input === "string" ? [Content.text(input)] : i
300
393
  /** Builds a full generate result, filling finish reason, usage, and warnings. */
301
394
  const generateResult = (input) => buildGenerateResult(typeof input === "string" ? { content: [Content.text(input)] } : input);
302
395
  /** Builds a full stream result; a string is assembled into `stream-start` → text → `finish`. */
303
- const streamResult = (input, opts = {}) => buildStreamResult(typeof input === "string" ? textToStream(input) : input, opts.initialDelayInMs, opts.chunkDelayInMs);
396
+ const streamResult = (input, opts = {}) => {
397
+ if (input instanceof ReadableStream) return { stream: input };
398
+ return buildStreamResult(typeof input === "string" ? textToStream(input) : input, opts);
399
+ };
304
400
  /** Builds a usage object, overriding individual token fields on top of the defaults. */
305
401
  const usage = (overrides = {}) => ({
306
402
  inputTokens: {
@@ -335,6 +431,50 @@ const MockLanguageModel = {
335
431
  finishReason
336
432
  };
337
433
 
434
+ //#endregion
435
+ //#region src/language/iterable.ts
436
+ /** Yields each array item in order as an async iterable. */
437
+ async function* fromArray(items) {
438
+ for (const item of items) yield item;
439
+ }
440
+ /**
441
+ * Operations for building, draining, and converting async iterables in tests.
442
+ *
443
+ * The complement to `Stream`: where `Stream` works with `ReadableStream`s, `Iterable` works with
444
+ * `AsyncIterable`s (async generators and anything consumed via `for await`). Cross over to the full
445
+ * `Stream` toolbox with `Iterable.toStream`.
446
+ */
447
+ const Iterable = {
448
+ from: (items) => fromArray(items),
449
+ toArray: async (iterable) => {
450
+ const items = [];
451
+ for await (const item of iterable) items.push(item);
452
+ return items;
453
+ },
454
+ toStream: (iterable) => {
455
+ const iterator = iterable[Symbol.asyncIterator]();
456
+ let cancelled = false;
457
+ return new ReadableStream({
458
+ async pull(controller) {
459
+ if (cancelled) return;
460
+ try {
461
+ const { value, done } = await iterator.next();
462
+ if (done) controller.close();
463
+ else controller.enqueue(value);
464
+ } catch (error) {
465
+ controller.error(error);
466
+ }
467
+ },
468
+ async cancel(reason) {
469
+ cancelled = true;
470
+ try {
471
+ await iterator.return?.(reason);
472
+ } catch {}
473
+ }
474
+ });
475
+ }
476
+ };
477
+
338
478
  //#endregion
339
479
  //#region src/language/options.ts
340
480
  /** Deterministic id generator so generated message ids are stable across runs. */
@@ -360,4 +500,4 @@ const Options = {
360
500
  };
361
501
 
362
502
  //#endregion
363
- export { Content, MockLanguageModel, Options, Stream, StreamParts };
503
+ export { Content, Iterable, MockLanguageModel, Options, Stream, StreamParts };
@@ -1,3 +1,8 @@
1
+ //#region src/internal/json.ts
2
+ /** Stringifies tool input to the JSON string the provider spec expects, leaving strings untouched. */
3
+ const toJSONString = (input) => typeof input === "string" ? input : JSON.stringify(input);
4
+
5
+ //#endregion
1
6
  //#region src/internal/tokenize.ts
2
7
  /**
3
8
  * Splits text into tokens. Shared by every streamed-text builder so chunking behavior lives
@@ -10,4 +15,4 @@ const tokenize = (text, { length, separator } = {}) => {
10
15
  };
11
16
 
12
17
  //#endregion
13
- export { tokenize as t };
18
+ export { toJSONString as n, tokenize as t };