ai-test-kit 0.0.1 → 1.0.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.
- package/README.md +79 -16
- package/dist/language/index.d.mts +36 -11
- package/dist/language/index.mjs +172 -32
- package/dist/{tokenize-C-Zp26iY.mjs → tokenize-Cy46iVSX.mjs} +6 -1
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.mjs +2 -2
- package/package.json +10 -13
package/README.md
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
# ai-test-kit
|
|
2
2
|
|
|
3
|
-
<p align="center">Test
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
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:
|
|
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[]
|
|
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>>; /**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
27
|
-
|
|
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) |
|
|
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 };
|
package/dist/language/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { t as tokenize } from "../tokenize-
|
|
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 = {}) =>
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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 = {}) =>
|
|
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 };
|
package/dist/ui/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import * as _ai_sdk_provider0 from "@ai-sdk/provider";
|
|
1
2
|
import * as ai from "ai";
|
|
2
3
|
import { UIDataTypes, UIMessage, UIMessageChunk, UIMessagePart, UIToolInvocation, UITools } from "ai";
|
|
3
|
-
import * as _ai_sdk_provider0 from "@ai-sdk/provider";
|
|
4
4
|
|
|
5
5
|
//#region src/ui/parts.d.ts
|
|
6
6
|
/** A single part variant selected from the union by its `type` tag. */
|
package/dist/ui/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as tokenize } from "../tokenize-
|
|
1
|
+
import { n as toJSONString, t as tokenize } from "../tokenize-Cy46iVSX.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/ui/parts.ts
|
|
4
4
|
/**
|
|
@@ -192,7 +192,7 @@ const createUIChunks = () => ({
|
|
|
192
192
|
toolCallId: args.toolCallId,
|
|
193
193
|
toolName: args.toolName
|
|
194
194
|
},
|
|
195
|
-
...tokenize(
|
|
195
|
+
...tokenize(toJSONString(args.input), { length: args.length }).map((inputTextDelta) => ({
|
|
196
196
|
type: "tool-input-delta",
|
|
197
197
|
toolCallId: args.toolCallId,
|
|
198
198
|
inputTextDelta
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-test-kit",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Test utilities for the AI SDK: mock models, content and stream-part builders, fully type-safe",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,16 +23,6 @@
|
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
|
-
"scripts": {
|
|
27
|
-
"prepublishOnly": "pnpm build",
|
|
28
|
-
"build": "tsdown",
|
|
29
|
-
"test": "vitest",
|
|
30
|
-
"lint": "oxlint --fix",
|
|
31
|
-
"lint:ci": "oxlint",
|
|
32
|
-
"format": "oxfmt --write",
|
|
33
|
-
"format:ci": "oxfmt --check",
|
|
34
|
-
"prepare": "husky"
|
|
35
|
-
},
|
|
36
26
|
"devDependencies": {
|
|
37
27
|
"@ai-sdk/provider": "^3.0.10",
|
|
38
28
|
"@ai-sdk/provider-utils": "^4.0.27",
|
|
@@ -67,5 +57,12 @@
|
|
|
67
57
|
"pnpm format"
|
|
68
58
|
]
|
|
69
59
|
},
|
|
70
|
-
"
|
|
71
|
-
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsdown",
|
|
62
|
+
"test": "vitest",
|
|
63
|
+
"lint": "oxlint --fix",
|
|
64
|
+
"lint:ci": "oxlint",
|
|
65
|
+
"format": "oxfmt --write",
|
|
66
|
+
"format:ci": "oxfmt --check"
|
|
67
|
+
}
|
|
68
|
+
}
|