ai-test-kit
v3.3.1
Published
Test utilities for the AI SDK: mock models, content and stream-part builders, fully type-safe
Readme
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.
Why?
The AI SDK ships MockLanguageModelV4 and a few other test primitives, but they are deliberately low-level. In practice every project ends up rebuilding the same helpers to:
- Mock a model: return text, throw an error, or replay a scripted response per call
- Generate content and stream parts: assemble valid
text-start→text-delta→text-end→finishstreams by hand - Simulate and consume streams: simulate streams with delays and drain them to assert on the chunks
This library turns that boilerplate into one-liners, ready to use. Models are vitest-compatible spy functions, so you can assert on calls with the full Vitest API while also reading the recorded call arguments directly (and the call record works under any test runner, or none).
Installation
[!NOTE] Version compatibility:
- Use
[email protected]for AI SDK v6 (provider specv3)- Use
[email protected]for AI SDK v7 (provider specv4)
npm install -D ai-test-kit@2 # for AI SDK v6
npm install -D ai-test-kit@3 # for AI SDK v7ai is a peer dependency. The spy engine ships with the kit via @vitest/spy, so Vitest itself is not required — the recorded call data works under any test runner, or none.
Usage
The API is split by layer, each under its own entry point so an import only pulls in the types it needs:
ai-test-kit— generic, layer-agnostic helpers: build, drain, and convertReadableStreams andAsyncIterables withStreamsandIterablesai-test-kit/language— the model layer: mock aLanguageModelV4and build the values it returns with theLanguagenamespace, both content forgenerateText()andstream-prefixed parts forstreamText()ai-test-kit/embedding— the embedding layer: mock anEmbeddingModelV4and build vectors and results with theEmbeddingnamespaceai-test-kit/image— the image layer: mock anImageModelV4and build sample images and results with theImagenamespaceai-test-kit/ui— the UI layer: buildUIMessagePart,UIMessageChunk, andUIMessagefixtures, optionally typed to your ownUIMessage
Streams and Iterables
Layer-agnostic helpers from the root entry ai-test-kit to build, drain, and convert ReadableStreams and AsyncIterables. They work with any part type, so they pair with every layer below (language stream parts, UI chunks, plain values).
import { Streams } from 'ai-test-kit';
import { Language } from 'ai-test-kit/language';
const parts = [...Language.streamText('Hello World'), Language.streamFinish()];
const drained = await Streams.toArray(Streams.from(parts)); // round-trips partsErrors
Layer-agnostic error builders from the root entry ai-test-kit for the provider errors a model throws. Drop one straight into a mock to drive retry, fallback, and error-handling tests. The named factories carry the realistic shape of each category (status code, message, Retry-After); Errors.from is the escape hatch for anything else.
import { generateText } from 'ai';
import { Errors } from 'ai-test-kit';
import { MockLanguageModel } from 'ai-test-kit/language';
const model = MockLanguageModel.from([Errors.rateLimited({ retryAfter: 5 }), 'Recovered']);
await generateText({ model, prompt: 'Hello' }).catch(() => {}); // first call throws a 429
const { text } = await generateText({ model, prompt: 'Hello' }); // second call recovers
text; // 'Recovered'isRetryable is left to APICallError, which derives it from the status code (408/409/429/5xx), matching how the AI SDK builds the error. retryAfter accepts seconds (number), an absolute Date, or { ms }, mapping to the retry-after / retry-after-ms headers a provider sends.
Language Models
Helpers from ai-test-kit/language to mock a model and build the content and stream parts it returns.
[!NOTE] This kit mocks at the
LanguageModelV4interface — the boundary between the AI SDK and a provider. It does not mock the HTTP/wire layer, so provider-specific wire behavior (e.g. an error inside a 200-OK SSE body) is out of scope. For that, drive a real provider against a fake server with@ai-sdk/test-server.
Creating a Mock Model
Pass a response to MockLanguageModel.from(). A string is the common case: it serves both doGenerate and doStream.
import { generateText } from 'ai';
import { MockLanguageModel } from 'ai-test-kit/language';
const model = MockLanguageModel.from('Hello, world!');
const result = await generateText({ model, prompt: 'Hi' });
result.text; // 'Hello, world!'Generate and Stream
The same model answers both generateText() and streamText(). For streaming, a string is assembled into a stream-start → text-start → text-delta* → text-end → finish sequence.
import { streamText } from 'ai';
import { Streams } from 'ai-test-kit';
import { MockLanguageModel } from 'ai-test-kit/language';
const model = MockLanguageModel.from('Hello World');
const result = streamText({ model, prompt: 'Hi' });
const text = (await Streams.toArray(result.textStream)).join(''); // 'Hello World'Throwing Errors
Pass an Error to make the model throw, for testing error handling and retries.
const model = MockLanguageModel.from(new Error('rate limited'));
await expect(generateText({ model, prompt: 'Hi' })).rejects.toThrow();Sequenced Responses
Pass an array to script a response per call. The model advances through the array and clamps to the last entry once exhausted, ideal for retry and fallback tests. An array models the sequence directly and is easy to build programmatically.
// fail, fail, then succeed
const model = MockLanguageModel.from([new Error('429'), new Error('429'), 'recovered']);
await generateText({ model, prompt: 'Hi' }).catch(() => {});
await generateText({ model, prompt: 'Hi' }).catch(() => {});
const result = await generateText({ model, prompt: 'Hi' });
result.text; // 'recovered'Building Content
Use the Language namespace to assemble the parts a model returns from doGenerate. Pass them via the content response form. Like a plain string, a { content } mock also serves streamText() — the stream is derived from the parts.
import { Language, MockLanguageModel } from 'ai-test-kit/language';
const model = MockLanguageModel.from({
content: [
Language.text('Here is the weather:'),
Language.toolCall({ toolCallId: 'call-1', toolName: 'weather', input: { city: 'Tokyo' } }),
],
});
const result = await generateText({ model, prompt: 'Weather in Tokyo?' });
result.toolCalls[0].toolName; // 'weather'The tool builders (toolCall, toolResult, streamToolInput) accept an optional tool-set generic. Pass your tool-set (e.g. <typeof tools>) and toolName is constrained to its keys, with input / result typed to the matching tool's input / output. Omit it and the builders stay loose (any name, unknown input), so existing call sites are unaffected.
import { Language } from 'ai-test-kit/language';
const tools = {
weather: tool({ inputSchema: z.object({ city: z.string() }), outputSchema: z.object({ tempC: z.number() }) }),
};
Language.toolCall<typeof tools>({ toolCallId: 'c1', toolName: 'weather', input: { city: 'Tokyo' } });
Language.toolCall<typeof tools>({ toolCallId: 'c1', toolName: 'weahter', input: { city: 'Tokyo' } }); // ✗ unknown tool name
Language.toolCall<typeof tools>({ toolCallId: 'c1', toolName: 'weather', input: { region: 'Tokyo' } }); // ✗ wrong input shapeUsage and Finish Reason
By default a mock reports a stop finish reason and a small fixed token usage. When you don't set a finish reason, it is derived from the content: a pending client tool call yields tool-calls, matching how a real provider reports it (a provider-executed call, or one whose result is already in the content, keeps stop). Override it 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; an explicit value always wins over derivation; Language.usage(...) builds the token usage.
import { Language, MockLanguageModel } from 'ai-test-kit/language';
const model = MockLanguageModel.from({
content: [Language.text('truncated…')],
finishReason: 'length',
usage: Language.usage({ outputTokens: { total: 50 } }),
});
const result = await generateText({ model, prompt: 'Hi' });
result.finishReason; // 'length'
result.usage; // the configured token usageBuilding Streams
Use the stream-prefixed Language builders to compose a stream from atoms. The text-like builders return a start / delta / end block (no trailing finish), so streams compose by concatenation.
import { Language, MockLanguageModel } from 'ai-test-kit/language';
const model = MockLanguageModel.from({
doStream: [
Language.streamStart(),
...Language.streamText('Hello', { length: 1 }), // emit one character per delta
...Language.streamToolInput({ id: 't1', toolName: 'weather', input: { city: 'Tokyo' } }),
Language.toolCall({ toolCallId: 'call-1', toolName: 'weather', input: { city: 'Tokyo' } }),
Language.streamFinish(),
],
});For the common stream-start → content → finish shape, Language.streamParts(...) builds the whole array in one call (the streaming sibling of Language.result), so you rarely assemble it by hand:
import { Language, MockLanguageModel } from 'ai-test-kit/language';
const model = MockLanguageModel.from({ doStream: Language.streamParts('Hello World') });
// for an error stream, compose explicitly:
const failing = MockLanguageModel.from({ doStream: [Language.streamStart(), Language.streamError(new Error('boom'))] });streamParts returns the raw parts array — splice it, snapshot it, or feed it to doStream as above. When you need a consumable { stream } instead — passing a ready stream, or returning from the function form of doStream — use Language.streamResult(...), which wraps the same string or parts and can be passed straight to the doStream form:
const model = MockLanguageModel.from({
doStream: ({ prompt }) => Language.streamResult(prompt.length > 0 ? 'has prompt' : 'empty'),
});
const ready = MockLanguageModel.from({ doStream: Language.streamResult('Hello World') });Delays
Streams can be made slow with initialDelayInMs / chunkDelayInMs, so a timing or deadline test needs no hand-rolled timer.
[!TIP] You can also use
Streams.simulate()to create a delayed stream.
const model = MockLanguageModel.from({
doStream: {
chunks: [...Language.streamText('This is a slow response', { separator: ' ' }), Language.streamFinish()],
initialDelayInMs: 10,
chunkDelayInMs: 5,
},
});A non-streaming call takes a single delayInMs instead. The call's abortSignal is wired in automatically: when it fires mid-delay the call rejects with the signal's own abort reason and the pending timer is cleared, exactly like a real provider cut short by a per-attempt deadline. That keeps the two kinds of cancellation apart the way a real call does, so a test can assert on which one fired: a signal from AbortSignal.timeout() surfaces as a TimeoutError, a plain controller.abort() as an AbortError.
import { generateText } from 'ai';
import { Language, MockLanguageModel } from 'ai-test-kit/language';
const model = MockLanguageModel.from({
doGenerate: { content: [Language.text('eventually')], delayInMs: 5_000 },
});
const result = generateText({ model, prompt: 'Hi', abortSignal: AbortSignal.timeout(1_000) });
// rejects with a TimeoutError after 1s, rather than resolving at 5sThe same delay works on the { error, ... } form, for a provider that times out instead of answering:
import { Errors } from 'ai-test-kit';
const model = MockLanguageModel.from({ doGenerate: { error: Errors.serviceUnavailable(), delayInMs: 5_000 } });Every delay is inert unless positive: 0, null, or an unset delay schedules no timer at all, so an undelayed mock stays safe under vi.useFakeTimers(). On a top-level { content, ... } response delayInMs applies to doGenerate only — a stream derived from the same object paces itself with the chunk options above. The same option works on MockEmbeddingModel's { embeddings, ... } and MockImageModel's { images, ... } forms.
Aborting a Stream
The call's abortSignal is wired into the simulated stream automatically, so a stream aborted mid-flight errors with the signal's own abort reason just like a real provider, no custom ReadableStream needed. As with a non-streaming call, that means a TimeoutError for a deadline and an AbortError for a manual cancellation. Pair it with chunkDelayInMs so the abort can land between chunks.
import { Language, MockLanguageModel } from 'ai-test-kit/language';
const controller = new AbortController();
const model = MockLanguageModel.from({
doStream: { chunks: [...Language.streamText('Hello World'), Language.streamFinish()], chunkDelayInMs: 10 },
});
const result = streamText({ model, prompt: 'Hi', abortSignal: controller.signal });
// ...later, controller.abort() makes the stream reject with an AbortErrorSingle-Method Mocks
from() drives both methods. When a test should exercise only one — and calling the other should fail loudly — use MockLanguageModel.stream() or MockLanguageModel.generate(). Each takes the same responses as the matching doStream / doGenerate key and leaves the other method unimplemented (it throws when called).
import { MockLanguageModel } from 'ai-test-kit/language';
const streaming = MockLanguageModel.stream('Hello World'); // doGenerate throws
const generating = MockLanguageModel.generate('Hello World'); // doStream throwsThey are sugar for from({ doStream }) / from({ doGenerate }), so every stream/generate response form works, including a pre-built Language.streamResult(...). To sequence responses across calls, use from() with an array.
Different Responses per Method
Use the { doGenerate, doStream } form to drive doGenerate and doStream independently — for example to return plain text non-streaming but a richer sequence when streamed. The keys mirror the LanguageModelV4 method names.
import { Language, MockLanguageModel } from 'ai-test-kit/language';
const model = MockLanguageModel.from({
doGenerate: 'Final answer',
doStream: [...Language.streamText('Final answer'), Language.streamFinish()],
});Input-Dependent Responses
For a response that depends on the call (the prompt, tools, or settings), pass a function to doGenerate / doStream. It receives the call options and returns the result directly — the escape hatch for cases the declarative forms can't express, including a fully custom LanguageModelV4StreamResult. The Language.result / Language.streamResult builders pair well here.
import { Language, MockLanguageModel } from 'ai-test-kit/language';
const model = MockLanguageModel.from({
doGenerate: async ({ prompt }) => Language.result(prompt.length > 1 ? 'multi-turn' : 'single-turn'),
});Deterministic Output
generateText() / streamText() assign a random response.id and a wall-clock response.timestamp. When a test asserts on those (e.g. snapshots), spread Options to pin them. It is not needed for ordinary assertions like result.text.
import { MockLanguageModel, Options } from 'ai-test-kit/language';
const model = MockLanguageModel.from('Hi');
await generateText({ model, prompt: 'x', ...Options.generate });
streamText({ model, prompt: 'x', ...Options.stream });Inspecting Calls
doGenerate and doStream are vitest-compatible spy functions (powered by @vitest/spy), so the full Vitest spy API and matchers work. Every call is recorded on doGenerate.mock.calls / doStream.mock.calls, which you can read without the Vitest runner (so the kit works under any test runner, or none).
const model = MockLanguageModel.from('hi');
await generateText({ model, prompt: 'question' });
// Vitest matchers
expect(model.doGenerate).toHaveBeenCalledTimes(1);
// Recorded call options (each entry is the call's argument tuple)
model.doGenerate.mock.calls.length; // 1
model.doGenerate.mock.calls[0][0].prompt;Custom Identity
Override provider and modelId; otherwise the model uses mock-provider and an auto-incrementing id.
const model = MockLanguageModel.from('hi', { provider: 'acme', modelId: 'acme-1' });
model.provider; // 'acme'
model.modelId; // 'acme-1'Embedding Models
Helpers from ai-test-kit/embedding to mock an EmbeddingModelV4. MockEmbeddingModel.from() takes the embedding vectors directly; pass an Error to throw, a function for input-dependent output, or a top-level array to script responses per call. Embedding.vector(dimension?) builds a sample vector when you don't care about the exact numbers.
Mocking Embeddings
import { embed } from 'ai';
import { Embedding, MockEmbeddingModel } from 'ai-test-kit/embedding';
const model = MockEmbeddingModel.from([Embedding.vector()]);
const { embedding } = await embed({ model, value: 'Hello' });
embedding; // [0.1, 0.2, 0.3]Retry and Sequenced Responses
A top-level array scripts a response per call (advancing, then clamping to the last), so you can model a failure that recovers. An embeddings matrix (number[][]) is one level shallower than a sequence, so the two never collide.
const model = MockEmbeddingModel.from([new Error('429'), [[0.1, 0.2, 0.3]]]);
await embed({ model, value: 'Hello' }).catch(() => {}); // first call throws
const { embedding } = await embed({ model, value: 'Hello' }); // second call recoversDelays
doEmbed can be made slow, or slow to fail, for deadline tests: it settles only once delayInMs has elapsed, and the call's abortSignal is honored mid-delay. An unset or non-positive delay schedules no timer. The { embeddings, ... } and { error, ... } forms both carry it — see Delays for the full story.
const slowResult = MockEmbeddingModel.from({ embeddings: [Embedding.vector()], delayInMs: 5_000 });
const slowFailure = MockEmbeddingModel.from({ error: Errors.serviceUnavailable(), delayInMs: 5_000 });Image Models
Helpers from ai-test-kit/image to mock an ImageModelV4. MockImageModel.from() takes the generated images (base64 strings or binary data); Image.png() is a ready-made base64 1x1 PNG (also exported as base64Png1x1).
Mocking Image Generation
import { generateImage } from 'ai';
import { Image, MockImageModel } from 'ai-test-kit/image';
const model = MockImageModel.from([Image.png()]);
const { image } = await generateImage({ model, prompt: 'A sunset' });
image.base64; // the configured base64 PNGRetry and Sequenced Responses
const model = MockImageModel.from([new Error('429'), [Image.png()]]);
await generateImage({ model, prompt: 'A sunset' }).catch(() => {}); // first call throws
await generateImage({ model, prompt: 'A sunset' }); // second call recoversDelays
doGenerate can be made slow, or slow to fail, for deadline tests: it settles only once delayInMs has elapsed, and the call's abortSignal is honored mid-delay. An unset or non-positive delay schedules no timer. The { images, ... } and { error, ... } forms both carry it — see Delays for the full story.
const slowResult = MockImageModel.from({ images: [Image.png()], delayInMs: 5_000 });
const slowFailure = MockImageModel.from({ error: Errors.serviceUnavailable(), delayInMs: 5_000 });UI Messages
Helpers from ai-test-kit/ui to build the messages, parts, and chunks exchanged between the server and the client. Use them to test code that operates on UIMessage, UIMessageChunk, or UIMessagePart (custom transports, stream consumers, message reducers). The builders return plain objects shaped to the AI SDK's UI types, so the entry has no runtime cost beyond the types.
Building Parts, Chunks, and Messages
UIParts builds message parts, UIChunks builds stream chunks, and UIMessages builds whole messages from a string shortcut or an array of parts.
import { UIChunks, UIMessages, UIParts } from 'ai-test-kit/ui';
const message = UIMessages.assistant([
UIParts.text('Here is the weather:'),
UIParts.sourceUrl({ sourceId: 's1', url: 'https://example.com' }),
]);
const chunks = [
UIChunks.start(),
...UIChunks.text('Hello', { length: 1 }), // text-start → text-delta* → text-end
UIChunks.finish(),
];The text-like builders (UIChunks.text / UIChunks.reasoning / UIChunks.toolInput) return a block of atoms, so streams compose by concatenation. The individual atoms (UIChunks.textStart, textDelta, …) are also available.
Consuming a Chunk Stream
The chunk builders shine when testing code that consumes a UIMessageChunk stream. Streams (from the root ai-test-kit) is layer-agnostic, so it builds and drains chunk streams too.
import { Streams } from 'ai-test-kit';
import { UIChunks } from 'ai-test-kit/ui';
const chunks = [UIChunks.start(), ...UIChunks.text('Hello'), UIChunks.finish()];
const stream = Streams.from(chunks); // ReadableStream<UIMessageChunk>
// pass `stream` to the code under test (a transport, reducer, readUIMessageStream, …),
// or drain it to assert on the chunks directly
const received = await Streams.toArray(stream);
received.length; // 5Typing to Your Own UIMessage
By default data payloads, tool names, and message metadata are loose. Pass your own UIMessage type to fromUIMessage() once and the bound builders infer them.
import type { UIMessage } from 'ai';
import { fromUIMessage } from 'ai-test-kit/ui';
type MyUIMessage = UIMessage<
{ traceId: string }, // metadata
{ weather: { city: string } }, // data parts
{ search: { input: { q: string }; output: { hits: number } } } // tools
>;
const { UIParts, UIChunks, UIMessages } = fromUIMessage<MyUIMessage>();
UIChunks.data('weather', { city: 'Tokyo' }); // name + payload typed
UIChunks.messageMetadata({ traceId: 't1' }); // metadata typed
UIParts.tool('search', { toolCallId: 'c1', state: 'output-available', input: { q: 'cats' }, output: { hits: 3 } });API
Streams and Iterables
Generic, layer-agnostic helpers exported from the root ai-test-kit.
Streams
Operations for building, draining, and converting ReadableStreams.
.from(parts)
Streams.from<CHUNK>(chunks: CHUNK[]): ReadableStream<CHUNK>
// Streams.from([a, b]): ReadableStream emitting a, then b.simulate(chunks, options?)
Streams.simulate<CHUNK>(chunks: CHUNK[], options?: StreamDelayOptions): ReadableStream<CHUNK>
// Streams.simulate([a, b], { chunkDelayInMs: 5 }): ReadableStream emitting a, then b, with delays.toArray(stream)
Streams.toArray<CHUNK>(stream: ReadableStream<CHUNK>): Promise<CHUNK[]>
// Streams.toArray(stream): Promise<[a, b]>.toIterable(stream)
Streams.toIterable<CHUNK>(stream: ReadableStream<CHUNK>): ReadableStream<CHUNK> & AsyncIterable<CHUNK>
// for await (const chunk of Streams.toIterable(stream)) { ... } — consume a stream via for-awaitIterables
The async-iterable complement to Streams: build, drain, and convert AsyncIterables (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 Streams toolbox with .toStream.
.from(items)
Iterables.from<ITEM>(items: ITEM[]): AsyncIterable<ITEM>
// Iterables.from([a, b]): an async iterable yielding a, then b.toArray(iterable)
Iterables.toArray<ITEM>(iterable: AsyncIterable<ITEM>): Promise<ITEM[]>
// Iterables.toArray(iterable): Promise<[a, b]>.toStream(iterable)
Iterables.toStream<ITEM>(iterable: AsyncIterable<ITEM>): ReadableStream<ITEM>
// Iterables.toStream(iterable): a ReadableStream emitting a, then bErrors
Generic, layer-agnostic error builders exported from the root ai-test-kit.
Errors
Builders for the provider errors a mock model throws. The named factories set a realistic status code and message; isRetryable is left to APICallError, which derives it from the status (408/409/429/5xx).
.from(options?)
The generic APICallError builder; fills the fields a test rarely cares about (url, requestBodyValues).
Errors.from(options?: ApiCallErrorOptions): APICallError
// Errors.from({ statusCode: 500 }): a retryable APICallError (status-derived).rateLimited(options?)
Errors.rateLimited(options?: { message?: string; retryAfter?: RetryAfter }): APICallError
// status 429; retryAfter sets a retry-after / retry-after-ms header.serviceUnavailable(options?)
Errors.serviceUnavailable(options?: { message?: string; retryAfter?: RetryAfter }): APICallError
// status 503; retryAfter sets a retry-after / retry-after-ms header.serviceOverloaded(options?)
Errors.serviceOverloaded(options?: { message?: string }): APICallError
// status 529.internalServerError(options?)
Errors.internalServerError(options?: { message?: string }): APICallError
// status 500.badRequest(options?)
Errors.badRequest(options?: { message?: string }): APICallError
// status 400 (not retryable).unauthorized(options?)
Errors.unauthorized(options?: { message?: string }): APICallError
// status 401 (not retryable).timeout(options?)
Errors.timeout(options?: { message?: string }): DOMException
// a TimeoutError DOMException, as AbortSignal.timeout() produces.abort(options?)
Errors.abort(options?: { message?: string }): DOMException
// an AbortError DOMException, as controller.abort() producesLanguage Models
Builders and the mock model from ai-test-kit/language.
MockLanguageModel
Factory for the mock model. The model returned by .from() exposes doGenerate / doStream as vitest-compatible spy functions and records call options on doGenerate.mock.calls / doStream.mock.calls. Build the values a model returns with Language. The namespace and the instance type share the name.
.from(input?, options?)
Creates a mock LanguageModelV4 from a response spec (or a sequence of them).
MockLanguageModel.from(input?: MockResponse | MockResponse[], options?: MockLanguageModelOptions): MockLanguageModel
// MockLanguageModel.from('Hi'): a model returning 'Hi' from generate and stream
// MockLanguageModel.from(new Error('429')): a model that throws from generate and stream
// MockLanguageModel.from({ content: [Language.text('Hi')] }): a model returning those parts (stream derived from them)
// MockLanguageModel.from({ doGenerate: 'A', doStream: [...] }): drives doGenerate and doStream independently
// MockLanguageModel.from({ doGenerate: (options) => result }): a function of the call options (input-dependent)
// MockLanguageModel.from({ doGenerate: { content: [...], delayInMs: 5_000 } }): resolves after 5s, or aborts if the call's signal fires first
// MockLanguageModel.from({ doGenerate: { error: Errors.timeout(), delayInMs: 5_000 } }): fails slowly, after 5s
// MockLanguageModel.from([new Error('429'), 'ok']): sequences responses per call, clamping to the lastinputdefaults to a single response repeated for every call; an array sequences one response per call, clamped to the last. SeeMockResponse.options.providerdefaults tomock-provider;options.modelIddefaults to an auto-incrementingmock-model-{n}.
.callOptions(overrides?)
MockLanguageModel.callOptions(overrides?: Partial<LanguageModelV4CallOptions>): LanguageModelV4CallOptions
// MockLanguageModel.callOptions(): a valid options object with a default 'Hello!' user prompt
// MockLanguageModel.callOptions({ temperature: 0.5 }): the default plus the override — for calling model.doGenerate / doStream directly without castsLanguage
Builders for everything a language model returns. The static content parts (text, toolCall, …) go in the content form of MockResponse; the stream-prefixed parts (streamText, streamFinish, …) compose a doStream sequence; result / streamResult / usage wrap them into full results. The text-like stream builders return a start / delta / end block (without finish), so streams compose by concatenation. The stream prefix mirrors the SDK's generateText / streamText split.
Content parts
.text(text)
Language.text(text: string): LanguageModelV4Text
// Language.text('Hi'): { type: 'text', text: 'Hi' }.reasoning(text)
Language.reasoning(text: string): LanguageModelV4Reasoning
// Language.reasoning('Because...'): { type: 'reasoning', text: 'Because...' }.toolCall(args)
Language.toolCall<TOOLS extends ToolSet = never>(args: { toolCallId: string; toolName: string; input: unknown }): LanguageModelV4ToolCall
// Language.toolCall({ toolCallId: 'c1', toolName: 'weather', input: { city: 'Tokyo' } }): { type: 'tool-call', toolCallId: 'c1', toolName: 'weather', input: '{"city":"Tokyo"}' } — input is JSON-stringified unless already a string. Valid in content and streams.
// Pass <typeof tools> to constrain toolName to a tool key and input to that tool's input type; omit it to stay loose..toolResult(args)
Language.toolResult<TOOLS extends ToolSet = never>(args: { toolCallId: string; toolName: string; result: unknown; isError?: boolean }): LanguageModelV4ToolResult
// Language.toolResult({ toolCallId: 'c1', toolName: 'weather', result: { temp: 20 } }): { type: 'tool-result', toolCallId: 'c1', toolName: 'weather', result: { temp: 20 } }
// Pass <typeof tools> to constrain toolName to a tool key and result to that tool's output type; omit it to stay loose..toolApprovalRequest(args)
A request for the user to approve a tool call before it runs.
Language.toolApprovalRequest(args: { approvalId: string; toolCallId: string; providerMetadata?: SharedV4ProviderMetadata }): LanguageModelV4ToolApprovalRequest
// Language.toolApprovalRequest({ approvalId: 'a1', toolCallId: 'c1' }): { type: 'tool-approval-request', approvalId: 'a1', toolCallId: 'c1' }.file(args)
Language.file(args: { mediaType: string; data: string | Uint8Array }): LanguageModelV4File
// Language.file({ mediaType: 'image/png', data: 'abc' }): { type: 'file', mediaType: 'image/png', data: { type: 'data', data: 'abc' } }.source(args)
Language.source(args: { id: string; url: string; title?: string }): LanguageModelV4Source
// Language.source({ id: 's1', url: 'https://example.com' }): { type: 'source', sourceType: 'url', id: 's1', url: 'https://example.com' }.reasoningFile(args)
A file generated during reasoning. New in AI SDK 7.
Language.reasoningFile(args: { mediaType: string; data: string | Uint8Array }): LanguageModelV4ReasoningFile
// Language.reasoningFile({ mediaType: 'image/png', data: 'abc' }): { type: 'reasoning-file', mediaType: 'image/png', data: { type: 'data', data: 'abc' } }.custom(args)
Provider-specific custom content; kind is {provider}.{type}. New in AI SDK 7.
Language.custom(args: { kind: `${string}.${string}`; providerMetadata?: SharedV4ProviderMetadata }): LanguageModelV4CustomContent
// Language.custom({ kind: 'acme.thinking' }): { type: 'custom', kind: 'acme.thinking' }Stream parts
.streamTextStart(options?)
Language.streamTextStart(options?: { id?: string }): LanguageModelV4StreamPart
// Language.streamTextStart(): { type: 'text-start', id: '1' } — id defaults to '1'.streamTextDelta(text, options?)
Language.streamTextDelta(text: string, options?: { id?: string }): LanguageModelV4StreamPart
// Language.streamTextDelta('Hi'): { type: 'text-delta', id: '1', delta: 'Hi' } — the delta string comes first, mirroring streamText.streamTextEnd(options?)
Language.streamTextEnd(options?: { id?: string }): LanguageModelV4StreamPart
// Language.streamTextEnd(): { type: 'text-end', id: '1' }.streamText(text, options?)
Language.streamText(text: string | string[], options?: StreamPartOptions): LanguageModelV4StreamPart[]
// Language.streamText('Hi'): [{ type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hi' }, { type: 'text-end', id: '1' }]
// Language.streamText(['He', 'llo']): an explicit two-delta split (string[] used as the deltas verbatim; length/separator ignored)
// Composed from streamTextStart / streamTextDelta / streamTextEnd..streamReasoningStart(options?)
Language.streamReasoningStart(options?: { id?: string }): LanguageModelV4StreamPart
// Language.streamReasoningStart(): { type: 'reasoning-start', id: '1' }.streamReasoningDelta(text, options?)
Language.streamReasoningDelta(text: string, options?: { id?: string }): LanguageModelV4StreamPart
// Language.streamReasoningDelta('Hmm'): { type: 'reasoning-delta', id: '1', delta: 'Hmm' }.streamReasoningEnd(options?)
Language.streamReasoningEnd(options?: { id?: string }): LanguageModelV4StreamPart
// Language.streamReasoningEnd(): { type: 'reasoning-end', id: '1' }.streamReasoning(text, options?)
Language.streamReasoning(text: string | string[], options?: StreamPartOptions): LanguageModelV4StreamPart[]
// Language.streamReasoning('Hmm'): [{ type: 'reasoning-start', id: '1' }, { type: 'reasoning-delta', id: '1', delta: 'Hmm' }, { type: 'reasoning-end', id: '1' }]
// Composed from streamReasoningStart / streamReasoningDelta / streamReasoningEnd..streamToolInputStart(toolName, options?)
Language.streamToolInputStart(toolName: string, options?: { id?: string }): LanguageModelV4StreamPart
// Language.streamToolInputStart('weather', { id: 't1' }): { type: 'tool-input-start', id: 't1', toolName: 'weather' }.streamToolInputDelta(text, options?)
Language.streamToolInputDelta(text: string, options?: { id?: string }): LanguageModelV4StreamPart
// Language.streamToolInputDelta('{"city":"Tokyo"}', { id: 't1' }): { type: 'tool-input-delta', id: 't1', delta: '{"city":"Tokyo"}' }.streamToolInputEnd(options?)
Language.streamToolInputEnd(options?: { id?: string }): LanguageModelV4StreamPart
// Language.streamToolInputEnd({ id: 't1' }): { type: 'tool-input-end', id: 't1' }.streamToolInput(args)
Language.streamToolInput<TOOLS extends ToolSet = never>(args: { id: string; toolName: string; input: unknown; length?: number }): LanguageModelV4StreamPart[]
// Language.streamToolInput({ id: 't1', toolName: 'weather', input: { city: 'Tokyo' } }): [{ type: 'tool-input-start', id: 't1', toolName: 'weather' }, { type: 'tool-input-delta', id: 't1', delta: '{"city":"Tokyo"}' }, { type: 'tool-input-end', id: 't1' }]
// Pass <typeof tools> to constrain toolName to a tool key and input to that tool's input type; omit it to stay loose.
// Composed from streamToolInputStart / streamToolInputDelta / streamToolInputEnd..streamStart(warnings?)
Language.streamStart(warnings?: SharedV4Warning[]): LanguageModelV4StreamPart
// Language.streamStart(): { type: 'stream-start', warnings: [] }.streamFinish(options?)
Language.streamFinish(options?: { finishReason?: LanguageModelV4FinishReason | LanguageModelV4FinishReason['unified']; usage?: LanguageModelV4Usage; providerMetadata?: SharedV4ProviderMetadata }): LanguageModelV4StreamPart
// Language.streamFinish(): { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage }
// Language.streamFinish({ providerMetadata: { openai: { responseId: 'r1' } } }): extra fields pass through onto the part.streamError(error)
Language.streamError(error: unknown): LanguageModelV4StreamPart
// Language.streamError(new Error('boom')): { type: 'error', error: Error('boom') }.streamResponseMetadata(meta?)
Language.streamResponseMetadata(meta?: LanguageModelV4ResponseMetadata): LanguageModelV4StreamPart
// Language.streamResponseMetadata({ id: 'r1' }): { type: 'response-metadata', id: 'r1' }.streamRaw(rawValue)
Language.streamRaw(rawValue: unknown): LanguageModelV4StreamPart
// Language.streamRaw({ foo: 1 }): { type: 'raw', rawValue: { foo: 1 } }Results and usage
.usage(overrides?) / .usage(inputTotal, outputTotal)
Language.usage(overrides?: { inputTokens?: Partial<LanguageModelV4Usage['inputTokens']>; outputTokens?: Partial<LanguageModelV4Usage['outputTokens']> }): LanguageModelV4Usage
Language.usage(inputTotal: number, outputTotal: number): LanguageModelV4Usage
// Language.usage({ outputTokens: { total: 99 } }): { inputTokens: { total: 10, … }, outputTokens: { total: 99, … } }
// Language.usage(5, 8): each total mirrors into its primary sub-field — { inputTokens: { total: 5, noCache: 5, … }, outputTokens: { total: 8, text: 8, … } }.result(input, options?)
Language.result(input: string | LanguageModelV4Content[], options?: ResultOptions): LanguageModelV4GenerateResult
// Language.result('hi'): { content: [{ type: 'text', text: 'hi' }], finishReason: { unified: 'stop', raw: 'stop' }, usage, warnings: [] }
// Language.result([Language.text('hi')], { finishReason: 'length', usage: Language.usage(5, 8) }): finishReason coerced from a unified string; extra fields pass through
// When finishReason is omitted it is derived from content: a pending client tool call yields 'tool-calls', otherwise 'stop'..streamParts(input, options?)
The array-returning sibling of result: the full stream-start → content → finish parts for a response, as a plain array you can splice, snapshot, feed to a doStream mock, or wrap with streamResult. A string becomes one text part. options are the streamFinish options (finishReason, usage, passthrough), applied to the trailing finish part. Like result, an omitted finishReason is derived from content (a pending client tool call yields tool-calls); note the standalone streamFinish atom is content-blind and always defaults to stop. For an error stream, compose it explicitly: [Language.streamStart(), Language.streamError(e)].
Language.streamParts(input: string | LanguageModelV4Content[], options?): LanguageModelV4StreamPart[]
// Language.streamParts('Hello'): [stream-start, text-start, text-delta…, text-end, finish]
// Language.streamParts([Language.text('a'), Language.toolCall({…})], { finishReason: 'tool-calls', usage }): start → content → finish.streamResult(input, options?)
Language.streamResult(input: string | LanguageModelV4StreamPart[] | ReadableStream<LanguageModelV4StreamPart>, options?: StreamDelayOptions): LanguageModelV4StreamResult
// Language.streamResult('hi'): { stream } — a ReadableStream of stream-start → text → finish
// Language.streamResult([...Language.streamText('hi'), Language.streamFinish()]): { stream } — a ReadableStream of the given parts
// Language.streamResult(Streams.from(parts)): { stream } — wraps an existing ReadableStream as-is (delays ignored)Options
Determinism helpers to spread into generateText() / streamText().
.generateId()
Options.generateId(): string
// Options.generateId(): 'aitxt-mock-id'.generate
Options.generate: { _internal: { generateId } }
// generateText({ model, prompt: 'x', ...Options.generate }): a deterministic generateId.stream
Options.stream: { _internal: { generateId, now } }
// streamText({ model, prompt: 'x', ...Options.stream }): a deterministic generateId and now[!NOTE]
Options.streampins timestamps via_internal.now, but the AI SDK usesnew Date()directly on thefinish-steppart in the error streaming path. Tests that hit that path additionally needvi.useFakeTimers().
Embedding Models
Builders from ai-test-kit/embedding. MockEmbeddingModel is the mock factory; Embedding builds the values it returns. Each is exported as both a value and a type.
MockEmbeddingModel
The mock factory, and the type of a model created by .from().
.from(input?, options?)
Creates a mock EmbeddingModelV4 whose doEmbed is a vitest-compatible spy function. input is an Array<EmbeddingVector> (the embeddings), an Error, a full result, a function of the call options, or an Array of those to sequence responses per call. The { embeddings, ... } and { error, ... } forms also take a delayInMs to simulate a slow call. options is { provider?, modelId?, maxEmbeddingsPerCall?, supportsParallelCalls? }. Each call is recorded on doEmbed.mock.calls.
.callOptions(overrides?)
Builds a minimal valid EmbeddingModelV4CallOptions (default values: ['hello'], plus any overrides), for invoking doEmbed directly without casts.
Embedding
Builders for the values an embedding model returns.
.vector(dimension?)
Embedding.vector(dimension?: number): EmbeddingVector
// Embedding.vector(): [0.1, 0.2, 0.3] (default dimension 3)
// Embedding.vector(5): a 5-element sample vector.usage(tokens?)
Embedding.usage(tokens?: number): EmbeddingModelV4Result['usage']
// Embedding.usage(9): { tokens: 9 }.result(embeddings, overrides?)
Embedding.result(embeddings: EmbeddingVector[], overrides?: Omit<Partial<EmbeddingModelV4Result>, 'embeddings'>): EmbeddingModelV4Result
// Embedding.result([[0.1, 0.2]]): { embeddings: [[0.1, 0.2]], usage: { tokens: 0 }, warnings: [] } — fills default usage and warningsImage Models
Builders from ai-test-kit/image. MockImageModel is the mock factory; Image builds the values it returns. Each is exported as both a value and a type.
MockImageModel
The mock factory, and the type of a model created by .from().
.from(input?, options?)
Creates a mock ImageModelV4 whose doGenerate is a vitest-compatible spy function. input is the generated images (string[] / Uint8Array[]), an Error, a full result, a function of the call options, or an Array of those to sequence responses per call. The { images, ... } and { error, ... } forms also take a delayInMs to simulate a slow call. options is { provider?, modelId?, maxImagesPerCall? }. Each call is recorded on doGenerate.mock.calls.
.callOptions(overrides?)
Builds a minimal valid ImageModelV4CallOptions with all required keys present (default prompt: 'A test image', n: 1, the rest undefined/{}, plus any overrides), for invoking doGenerate directly without casts.
Image
Builders for the values an image model returns.
.png(size?)
Image.png(size?: ImageSize): string
// Image.png(): a valid base64 1x1 transparent PNG, handy as a stand-in generated image
// Image.png('1x1'): the same, with the size given explicitlyA valid base64 1x1 transparent PNG. size only accepts '1x1' today (the ImageSize type reserves room for more). The same string is also exported as the base64Png1x1 constant.
.usage(inputTokens?, outputTokens?)
Image.usage(inputTokens?: number, outputTokens?: number): ImageModelV4Usage
// Image.usage(3, 5): { inputTokens: 3, outputTokens: 5, totalTokens: 8 } — totalTokens defaults to the sum.result(images, overrides?)
Image.result(images: GeneratedImages, overrides?): ImageGenerateResult
// Image.result([Image.png()]): { images: [...], warnings: [], response: { timestamp: new Date(0), modelId: 'mock-model', headers: undefined } } — fills default warnings and a deterministic response (response may be partially overridden)UI Messages
Builders from ai-test-kit/ui. See UI Messages under Usage for examples. By default data, tool, and metadata are loosely typed; bind them with fromUIMessage.
UIParts
Builders for UIMessagePart.
.text(text, options?)
UIParts.text(text: string, options?: { state?: 'streaming' | 'done'; providerMetadata?: ProviderMetadata }): TextUIPart
// UIParts.text('Hi'): { type: 'text', text: 'Hi' }.reasoning(text, options?)
UIParts.reasoning(text: string, options?: { state?: 'streaming' | 'done'; providerMetadata?: ProviderMetadata }): ReasoningUIPart
// UIParts.reasoning('Hmm'): { type: 'reasoning', text: 'Hmm' }.sourceUrl(args)
UIParts.sourceUrl(args: { sourceId: string; url: string; title?: string; providerMetadata?: ProviderMetadata }): SourceUrlUIPart
// UIParts.sourceUrl({ sourceId: 's1', url: 'https://example.com' }): { type: 'source-url', sourceId: 's1', url: 'https://example.com' }.sourceDocument(args)
UIParts.sourceDocument(args: { sourceId: string; mediaType: string; title: string; filename?: string; providerMetadata?: ProviderMetadata }): SourceDocumentUIPart
// UIParts.sourceDocument({ sourceId: 's1', mediaType: 'application/pdf', title: 'Doc' }): { type: 'source-document', sourceId: 's1', mediaType: 'application/pdf', title: 'Doc' }.file(args)
UIParts.file(args: { mediaType: string; filename?: string; url: string; providerMetadata?: ProviderMetadata }): FileUIPart
// UIParts.file({ mediaType: 'image/png', url: 'https://example.com/a.png' }): { type: 'file', mediaType: 'image/png', url: 'https://example.com/a.png' }.reasoningFile(args)
A file generated during reasoning, referenced by URL. New in AI SDK 7.
UIParts.reasoningFile(args: { mediaType: string; url: string; providerMetadata?: ProviderMetadata }): ReasoningFileUIPart
// UIParts.reasoningFile({ mediaType: 'image/png', url: 'https://example.com/r.png' }): { type: 'reasoning-file', mediaType: 'image/png', url: 'https://example.com/r.png' }.custom(args)
Provider-specific custom part; kind is {provider}.{type}. New in AI SDK 7.
UIParts.custom(args: { kind: `${string}.${string}`; providerMetadata?: ProviderMetadata }): CustomContentUIPart
// UIParts.custom({ kind: 'acme.box' }): { type: 'custom', kind: 'acme.box' }.stepStart()
UIParts.stepStart(): StepStartUIPart
// UIParts.stepStart(): { type: 'step-start' }.data(name, data, options?)
UIParts.data(name: string, data: unknown, options?: { id?: string }): DataUIPart
// UIParts.data('weather', { city: 'Tokyo' }): { type: 'data-weather', data: { city: 'Tokyo' } }.tool(name, invocation)
UIParts.tool(name: string, invocation: UIToolInvocation): ToolUIPart
// UIParts.tool('weather', { toolCallId: 'c1', state: 'output-available', input: { city: 'Tokyo' }, output: { temp: 20 } }): { type: 'tool-weather', toolCallId: 'c1', state: 'output-available', input: { city: 'Tokyo' }, output: { temp: 20 } }.dynamicTool(invocation)
UIParts.dynamicTool(invocation: Omit<DynamicToolUIPart, 'type'>): DynamicToolUIPart
// UIParts.dynamicTool({ toolName: 'weather', toolCallId: 'c1', state: 'input-available', input: { city: 'Tokyo' } }): { type: 'dynamic-tool', toolName: 'weather', toolCallId: 'c1', state: 'input-available', input: { city: 'Tokyo' } }UIChunks
Builders for every UIMessageChunk variant; required fields shown, each also accepts its variant's optional fields (providerMetadata, toolMetadata, …). The text, reasoning, and toolInput block helpers return arrays.
.textStart(args)
UIChunks.textStart(args: { id: string }): UIMessageChunk
// UIChunks.textStart({ id: '1' }): { type: 'text-start', id: '1' }.textDelta(args)
UIChunks.textDelta(args: { id: string; delta: string }): UIMessageChunk
// UIChunks.textDelta({ id: '1', delta: 'Hi' }): { type: 'text-delta', id: '1', delta: 'Hi' }.textEnd(args)
UIChunks.textEnd(args: { id: string }): UIMessageChunk
// UIChunks.textEnd({ id: '1' }): { type: 'text-end', id: '1' }.reasoningStart(args)
UIChunks.reasoningStart(args: { id: string }): UIMessageChunk
// UIChunks.reasoningStart({ id: 'r1' }): { type: 'reasoning-start', id: 'r1' }.reasoningDelta(args)
UIChunks.reasoningDelta(args: { id: string; delta: string }): UIMessageChunk
// UIChunks.reasoningDelta({ id: 'r1', delta: 'Hmm' }): { type: 'reasoning-delta', id: 'r1', delta: 'Hmm' }.reasoningEnd(args)
UIChunks.reasoningEnd(args: { id: string }): UIMessageChunk
// UIChunks.reasoningEnd({ id: 'r1' }): { type: 'reasoning-end', id: 'r1' }.error(errorText)
UIChunks.error(errorText: string): UIMessageChunk
// UIChunks.error('boom'): { type: 'error', errorText: 'boom' }.toolInputStart(args)
UIChunks.toolInputStart(args: { toolCallId: string; toolName: string }): UIMessageChunk
// UIChunks.toolInputStart({ toolCallId: 'c1', toolName: 'weather' }): { type: 'tool-input-start', toolCallId: 'c1', toolName: 'weather' }.toolInputDelta(args)
UIChunks.toolInputDelta(args: { toolCallId: string; inputTextDelta: string }): UIMessageChunk
// UIChunks.toolInputDelta({ toolCallId: 'c1', inputTextDelta: '{"city":' }): { type: 'tool-input-delta', toolCallId: 'c1', inputTextDelta: '{"city":' }.toolInputAvailable(args)
UIChunks.toolInputAvailable(args: { toolCallId: string; toolName: string; input: unknown }): UIMessageChunk
// UIChunks.toolInputAvailable({ toolCallId: 'c1', toolName: 'weather', input: { city: 'Tokyo' } }): { type: 'tool-input-available', toolCallId: 'c1', toolName: 'weather', input: { city: 'Tokyo' } }.toolInputError(args)
UIChunks.toolInputError(args: { toolCallId: string; toolName: string; input: unknown; errorText: string }): UIMessageChunk
// UIChunks.toolInputError({ toolCallId: 'c1', toolName: 'weather', input: {}, errorText: 'bad input' }): { type: 'tool-input-error', toolCallId: 'c1', toolName: 'weather', input: {}, errorText: 'bad input' }.toolApprovalRequest(args)
UIChunks.toolApprovalRequest(args: { approvalId: string; toolCallId: string }): UIMessageChunk
// UIChunks.toolApprovalRequest({ approvalId: 'a1', toolCallId: 'c1' }): { type: 'tool-approval-request', approvalId: 'a1', toolCallId: 'c1' }.toolApprovalResponse(args)
Grants or denies a tool approval request. New in AI SDK 7.
UIChunks.toolApprovalResponse(args: { approvalId: string; approved: boolean; reason?: string; providerExecuted?: boolean }): UIMessageChunk
// UIChunks.toolApprovalResponse({ approvalId: 'a1', approved: true }): { type: 'tool-approval-response', approvalId: 'a1', approved: true }.toolOutputAvailable(args)
UIChunks.toolOutputAvailable(args: { toolCallId: string; output: unknown }): UIMessageChunk
// UIChunks.toolOutputAvailable({ toolCallId: 'c1', output: { temp: 20 } }): { type: 'tool-output-available', toolCallId: 'c1', output: { temp: 20 } }.toolOutputError(args)
UIChunks.toolOutputError(args: { toolCallId: string; errorText: string }): UIMessageChunk
// UIChunks.toolOutputError({ toolCallId: 'c1', errorText: 'failed' }): { type: 'tool-output-error', toolCallId: 'c1', errorText: 'failed' }.toolOutputDenied(args)
UIChunks.toolOutputDenied(args: { toolCallId: string }): UIMessageChunk
// UIChunks.toolOutputDenied({ toolCallId: 'c1' }): { type: 'tool-output-denied', toolCallId: 'c1' }.sourceUrl(args)
UIChunks.sourceUrl(args: { sourceId: string; url: string; title?: string }): UIMessageChunk
// UIChunks.sourceUrl({ sourceId: 's1', url: 'https://example.com' }): { type: 'source-url', sourceId: 's1', url: 'https://example.com' }.sourceDocument(args)
UIChunks.sourceDocument(args: { sourceId: string; mediaType: string; title: string; filename?: string }): UIMessageChunk
// UIChunks.sourceDocument({ sourceId: 's1', mediaType: 'application/pdf', title: 'Doc' }): { type: 'source-document', sourceId: 's1', mediaType: 'application/pdf', title: 'Doc' }.file(args)
UIChunks.file(args: { url: string; mediaType: string }): UIMessageChunk
// UIChunks.file({ url: 'https://example.com/a.png', mediaType: 'image/png' }): { type: 'file', url: 'https://example.com/a.png', mediaType: 'image/png' }.reasoningFile(args)
A file generated during reasoning, referenced by URL. New in AI SDK 7.
UIChunks.reasoningFile(args: { url: string; mediaType: string; providerMetadata?: ProviderMetadata }): UIMessageChunk
// UIChunks.reasoningFile({ url: 'https://example.com/r.png', mediaType: 'image/png' }): { type: 'reasoning-file', url: 'https://example.com/r.png', mediaType: 'image/png' }.custom(args)
Provider-specific custom content; kind is {provider}.{type}. New in AI SDK 7.
UIChunks.custom(args: { kind: `${string}.${string}`; providerMetadata?: ProviderMetadata }): UIMessageChunk
// UIChunks.custom({ kind: 'acme.box' }): { type: 'custom', kind: 'acme.box' }.data(name, data, options?)
UIChunks.data(name: string, data: unknown, options?: { id?: string; transient?: boolean }): UIMessageChunk
// UIChunks.data('weather', { city: 'Tokyo' }): { type: 'data-weather', data: { city: 'Tokyo' } }.startStep()
UIChunks.startStep(): UIMessageChunk
// UIChunks.startStep(): { type: 'start-step' }.finishStep()
UIChunks.finishStep(): UIMessageChunk
// UIChunks.finishStep(): { type: 'finish-step' }.start(args?)
UIChunks.start(args?: { messageId?: string; messageMetadata?: unknown }): UIMessageChunk
// UIChunks.start(): { type: 'start' }.finish(args?)
UIChunks.finish(args?: { finishReason?: FinishReason; messageMetadata?: unknown }): UIMessageChunk
// UIChunks.finish(): { type: 'finish' }.abort(args?)
UIChunks.abort(args?: { reason?: string }): UIMessageChunk
// UIChunks.abort({ reason: 'cancelled' }): { type: 'abort', reason: 'cancelled' }.messageMetadata(metadata)
UIChunks.messageMetadata(metadata: unknown): UIMessageChunk
// UIChunks.messageMetadata({ traceId: 't1' }): { type: 'message-metadata', messageMetadata: { traceId: 't1' } }.text(text, options?)
UIChunks.text(text: string, options?: UIChunkBlockOptions): UIMessageChunk[]
// UIChunks.text('Hi'): [{ type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hi' }, { type: 'text-end', id: '1' }].reasoning(text, options?)
UIChunks.reasoning(text: string, options?: UIChunkBlockOptions): UIMessageChunk[]
// UIChunks.reasoning('Hmm'): [{ type: 'reasoning-start', id: '1' }, { type: 'reasoning-delta', id: '1', delta: 'Hmm' }, { type: 'reasoning-end', id: '1' }].toolInput(args)
UIChunks.toolInput(args: { toolCallId: string; toolName: string; input: unknown; length?: number }): UIMessageChunk[]
// UIChunks.toolInput({ toolCallId: 'c1', toolName: 'weather', input: { city: 'Tokyo' } }): [{ type: 'tool-input-start', toolCallId: 'c1', toolName: 'weather' }, { type: 'tool-input-delta', toolCallId: 'c1', inputTextDelta: '{"city":"Tokyo"}' }, { type: 'tool-input-available', toolCallId: 'c1', toolName: 'weather', input: { city: 'Tokyo' } }]UIMessages
Builders for UIMessage. A string becomes a single text part; ids auto-increment (mock-message-{n}) when omitted.
.user(content, options?)
UIMessages.user(content: string | UIMessagePart[], options?: { id?: string; metadata?: unknown }): UIMessage
// UIMessages.user('hi'): { id: 'mock-message-1', role: 'user', parts: [{ type: 'text', text: 'hi' }] }
// UIMessages.user([UIParts.text('hi')]): { id: 'mock-message-1', role: 'user', parts: [{ type: 'text', text: 'hi' }] } — array of parts passes through.assistant(content, options?)
UIMessages.assistant(content: string | UIMessagePart[], options?: { id?: string; metadata?: unknown }): UIMessage
// UIMessages.assistant('Hi'): { id: 'mock-message-1', role: 'assistant', parts: [{ type: 'text', text: 'Hi' }] }
// UIMessages.assistant([UIParts.text('Hi')], { id: 'm1' }): { id: 'm1', role: 'assistant', parts: [{ type: 'text', text: 'Hi' }] } — array of parts passes through.system(content, options?)
UIMessages.system(content: string | UIMessagePart[], options?: { id?: string; metadata?: unknown }): UIMessage
// UIMessages.system('Be concise', { id: 'm1' }): { id: 'm1', role: 'system', parts: [{ type: 'text', text: 'Be concise' }] }
// UIMessages.system([UIParts.text('Be concise')]): { id: 'mock-message-1', role: 'system', parts: [{ type: 'text', text: 'Be concise' }] } — array of parts passes throughfromUIMessage
Binds the UI builders to a concrete UIMessage type so data, tool, and metadata infer their names and payloads. The createUIParts / createUIChunks / createUIMessages factories are also exported for binding type parameters directly.
fromUIMessage<UIMessage>(): { UIParts; UIChunks; UIMessages }
// const { UIParts, UIChunks, UIMessages } = fromUIMessage<MyUIMessage>(): builders typed to MyUIMessageTypes
Types are split by entry point, mirroring the builders.
Streams and Iterables
Exported from the root ai-test-kit.
StreamDelayOptions
Simulated timing shared by Streams.simulate, Language.streamResult, and the doStream chunks form. With an abortSignal, the stream errors the instant the signal fires (mid-delay) with that signal's own abort reason, matching a real provider stream: a TimeoutError for AbortSignal.timeout(), an AbortError for controller.abort(). By default there is no delay and no timer, so a string / { content } / bare-array doStream stays inert under vi.useFakeTimers() (only a positive initialDelayInMs / chunkDelayInMs schedules a real setTimeout); a bare array therefore drains like Streams.from.
import type { StreamDelayOptions } from 'ai-test-kit';
// { initialDelayInMs?: number | null; chunkDelayInMs?: number | null; abortSignal?: AbortSignal }CallDelayOptions
Simulated latency for a non-streaming call, shared by the object response forms of MockLanguageModel's doGenerate, MockEmbeddingModel's doEmbed, and MockImageModel's doGenerate — including their { error, ... } forms, so a failure can be slow too. There is no abortSignal to configure: the mock reads the one on the call it received, and rejects with that signal's own abort reason (clearing its timer) the instant it fires mid-delay: a TimeoutError for AbortSignal.timeout(), an AbortError for controller.abort(). By default there is no delay and no timer, so an undelayed mock stays inert under vi.useFakeTimers(); only a positive delayInMs schedules a real setTimeout.
import type { CallDelayOptions } from 'ai-test-kit';
// { delayInMs?: number | null }Errors
Exported from the root ai-test-kit.
RetryAfter
A Retry-After value in any form a provider sends it: number (seconds, emitted as retry-after), Date (HTTP-date, emitted as retry-after), or { ms } (milliseconds, emitted as retry-after-ms).
import type { RetryAfter } from 'ai-test-kit';
// number | Date | { ms: number }ApiCallErrorOptions
The options accepted by Errors.from. Everything is optional; the noise fields default to inert values, and an omitted isRetryable is derived from the status code by APICallError.
import type { ApiCallErrorOptions } from 'ai-test-kit';
// { message?; statusCode?; isRetryable?; responseHeaders?; responseBody?; url?; requestBodyValues?; data?; cause? }Language Models
Exported from ai-test-kit/language.
MockResponse
A single mock response. A string or Error applies to whichever method is called; the object forms target one method explicitly. Pass an Array<MockResponse> to sequence responses across calls.
type MockResponse =
| string // text, for both generate and stream
| Error // both methods throw
| { content; finishReason?; usage?; delayInMs? } // generate result (delayed), or a derived stream
| { doGenerate?; doStream? }; // doGenerate and/or doStream explicitlyMockLanguageModel
The mock model instance type, as returned by MockLanguageModel.from(). Because the namespace and the instance type share the name, you can use MockLanguageModel to annotate a model parameter.
import type { MockLanguageModel } from 'ai-test-kit/language';GenerateResponse / StreamResponse
The per-method response shapes used by the { doGenerate, doStream } form of MockResponse. doStream accepts a bare Array<StreamPart>, a ReadableStream<StreamPart> (used as-is), or a { chunks, initialDelayInMs?, chunkDelayInMs? } object to simulate delays. doGenerate accepts a { content, ..., delayInMs? } or { error, delayInMs? } object to simulate a slow answer or a slow failure — see CallDelayOptions. 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).
import type { GenerateResponse, StreamResponse } from 'ai-test-kit/language';MockLanguageModelOptions
The identity overrides accepted as the second argument to MockLanguageModel.from().
import type { MockLanguageModelOptions } from 'ai-test-kit/language';
// { provider?: string; modelId?: string }StreamPartOptions
Options for the streamed-text block builders (Language.streamText / Language.streamReasoning).
import type { StreamPartOptions } from 'ai-test-kit/language';
// { id?: string; length?: number; separator?: string }ResultOptions
Options for Language.result: everything defaults, and extra fields (e.g. providerMetadata) pass through. finishReason accepts a full object or a bare unified value; usage defaults to a small stable value.
import type { ResultOptions } from 'ai-test-kit/language';
// Omit<Partial<LanguageModelV4GenerateResult>, 'content' | 'finishReason' | 'usage'> & { finishReason?; usage? }Embedding Models
Exported from ai-test-kit/embedding.
MockEmbeddingModel
The mock model instance type, as returned by MockEmbeddingModel.from(). The namespace and the instance type share the name, so MockEmbeddingModel annotates a model parameter.
EmbedResponse
How to respond to a doEmbed call: an Array<EmbeddingVector>, an Error, a (partial) result, an { error, delayInMs? } failure, or a function of the call options. The two object forms take a delayInMs.
import type { EmbedResponse, EmbeddingVector, MockEmbeddingModelOptions } from 'ai-test-kit/embedding';MockEmbeddingModelOptions
Identity and capability overrides accepted as the second argument to .from().
// { provider?: string; modelId?: string; maxEmbeddingsPerCall?: number; supportsParallelCalls?: boolean }Image Models
Exported from ai-test-kit/image.
MockImageModel
The mock model instance type, as returned by MockImageModel.from(). The namespace and the instance type share the name, so MockImageModel annotates
