ai-test-kit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export { };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,218 @@
1
+ import { Mock } from "vitest";
2
+ import { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3Content, LanguageModelV3File, LanguageModelV3FinishReason, LanguageModelV3GenerateResult, LanguageModelV3Reasoning, LanguageModelV3Source, LanguageModelV3StreamPart, LanguageModelV3StreamResult, LanguageModelV3Text, LanguageModelV3ToolCall, LanguageModelV3ToolResult, LanguageModelV3Usage } from "@ai-sdk/provider";
3
+
4
+ //#region src/language/stream.d.ts
5
+ /** Simulated timing for a stream. Shared by `Stream.simulate`, `MockLanguageModel.streamResult`, and the `stream` chunks form. */
6
+ type StreamDelayOptions = {
7
+ /** Delay before the first part is emitted. */initialDelayInMs?: number; /** Delay between each subsequent part. */
8
+ chunkDelayInMs?: number;
9
+ };
10
+ /** Operations for building, draining, and inspecting language model streams in tests. */
11
+ declare const Stream: {
12
+ /** 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
+ simulate: <PART>(chunks: Array<PART>, opts?: StreamDelayOptions) => ReadableStream<PART>; /** Reads a stream to completion and returns every part it emitted. */
14
+ toArray: <PART>(stream: ReadableStream<PART>) => Promise<Array<PART>>; /** Joins the `text-delta` parts of a stream-part sequence into the full text. */
15
+ text: (parts: Array<LanguageModelV3StreamPart>) => string; /** Returns the finish reason from a stream-part sequence, if a `finish` part is present. */
16
+ finishReason: (parts: Array<LanguageModelV3StreamPart>) => LanguageModelV3FinishReason | undefined;
17
+ };
18
+ //#endregion
19
+ //#region src/language/mock-language-model.d.ts
20
+ /** A (possibly partial) non-streaming result; only `content` is required, the rest defaults. */
21
+ type GenerateResultInput = Partial<LanguageModelV3GenerateResult> & {
22
+ content: Array<LanguageModelV3Content>;
23
+ };
24
+ /** How to respond to a `doGenerate` call. */
25
+ type GenerateResponse = string | Error | GenerateResultInput | LanguageModelV3['doGenerate'];
26
+ /** How to respond to a `doStream` call. A bare array streams without delay; the object form adds delays. */
27
+ type StreamResponse = string | Error | Array<LanguageModelV3StreamPart> | ({
28
+ chunks: Array<LanguageModelV3StreamPart>;
29
+ } & StreamDelayOptions) | LanguageModelV3StreamResult | LanguageModelV3['doStream'];
30
+ /**
31
+ * A single mock response. A `string` or `Error` applies to whichever method is called;
32
+ * the object forms target one method explicitly.
33
+ *
34
+ * Note: `string` and `{ content }` describe the output for both methods — when streamed, a stream is
35
+ * derived from the content. To sequence responses across calls, pass an `Array<MockResponse>` at the
36
+ * top level. Because of that, a raw stream is expressed via the `stream` form (`{ stream: [...] }`),
37
+ * never a bare array.
38
+ */
39
+ type MockResponse = string | Error | GenerateResultInput | {
40
+ generate?: GenerateResponse;
41
+ stream?: StreamResponse;
42
+ };
43
+ /** Optional identity overrides for a mock model. */
44
+ type MockLanguageModelOptions = {
45
+ /** The provider id; defaults to `mock-provider`. */provider?: string; /** The model id; defaults to an auto-incrementing `mock-model-{n}`. */
46
+ modelId?: string;
47
+ };
48
+ /**
49
+ * A `LanguageModelV3` mock whose `doGenerate`/`doStream` are `vi.fn()` spies. Each call is also
50
+ * recorded on `doGenerateCalls`/`doStreamCalls` so call arguments can be inspected without vitest.
51
+ * Instances are created via the {@link MockLanguageModel} factory.
52
+ */
53
+ declare class LanguageModelMock implements LanguageModelV3 {
54
+ /** The language model spec version this mock implements. */
55
+ readonly specificationVersion = "v3";
56
+ /** URL patterns the model supports — none, for a mock. */
57
+ readonly supportedUrls: LanguageModelV3['supportedUrls'];
58
+ /** The provider id. */
59
+ readonly provider: string;
60
+ /** The model id. */
61
+ readonly modelId: string;
62
+ /** Spy implementing `doGenerate`, resolving the configured response. */
63
+ doGenerate: Mock<LanguageModelV3['doGenerate']>;
64
+ /** Spy implementing `doStream`, resolving the configured response. */
65
+ doStream: Mock<LanguageModelV3['doStream']>;
66
+ /** Call options captured for every `doGenerate` invocation, in order. */
67
+ doGenerateCalls: Array<LanguageModelV3CallOptions>;
68
+ /** Call options captured for every `doStream` invocation, in order. */
69
+ doStreamCalls: Array<LanguageModelV3CallOptions>;
70
+ /** Builds the spies and identity from the configured response(s) and options. */
71
+ constructor(input?: MockResponse | Array<MockResponse>, options?: MockLanguageModelOptions);
72
+ }
73
+ /**
74
+ * Namespace for building mock language models. `from` creates a mock `LanguageModelV3`; the other
75
+ * builders assemble the values a model returns. Exported as both a value (the namespace) and a type
76
+ * (the model instance).
77
+ *
78
+ * @example
79
+ * const model = MockLanguageModel.from('Hello, world!');
80
+ * const flaky = MockLanguageModel.from([new Error('rate limited'), 'recovered']);
81
+ * const built = MockLanguageModel.from({ content: MockLanguageModel.content('Hi') });
82
+ */
83
+ declare const MockLanguageModel: {
84
+ from: (input?: MockResponse | Array<MockResponse>, options?: MockLanguageModelOptions) => LanguageModelMock;
85
+ content: (input: string | Array<LanguageModelV3Content>) => Array<LanguageModelV3Content>;
86
+ generateResult: (input: string | GenerateResultInput) => LanguageModelV3GenerateResult;
87
+ streamResult: (input: string | Array<LanguageModelV3StreamPart>, opts?: StreamDelayOptions) => LanguageModelV3StreamResult;
88
+ usage: (overrides?: {
89
+ inputTokens?: Partial<LanguageModelV3Usage["inputTokens"]>;
90
+ outputTokens?: Partial<LanguageModelV3Usage["outputTokens"]>;
91
+ }) => LanguageModelV3Usage;
92
+ finishReason: (unified?: LanguageModelV3FinishReason["unified"]) => LanguageModelV3FinishReason;
93
+ };
94
+ /** A mock language model instance, as returned by {@link MockLanguageModel.from}. */
95
+ type MockLanguageModel = LanguageModelMock;
96
+ //#endregion
97
+ //#region src/language/content.d.ts
98
+ /** Builders for the static content parts a language model returns from `doGenerate`. */
99
+ declare const Content: {
100
+ /** A text part. */text: (text: string) => LanguageModelV3Text; /** A reasoning part. */
101
+ reasoning: (text: string) => LanguageModelV3Reasoning; /** A tool call. `input` is stringified to JSON unless already a string. */
102
+ toolCall: (args: {
103
+ toolCallId: string;
104
+ toolName: string;
105
+ input: unknown;
106
+ }) => LanguageModelV3ToolCall; /** A tool result. */
107
+ toolResult: (args: {
108
+ toolCallId: string;
109
+ toolName: string;
110
+ result: LanguageModelV3ToolResult["result"];
111
+ isError?: boolean;
112
+ }) => LanguageModelV3ToolResult; /** A file part. */
113
+ file: (args: {
114
+ mediaType: string;
115
+ data: string | Uint8Array;
116
+ }) => LanguageModelV3File; /** A URL source part. */
117
+ source: (args: {
118
+ id: string;
119
+ url: string;
120
+ title?: string;
121
+ }) => LanguageModelV3Source;
122
+ };
123
+ //#endregion
124
+ //#region src/language/stream-parts.d.ts
125
+ /** The `warnings` array carried by a `stream-start` part. */
126
+ type StreamStartWarnings = Extract<LanguageModelV3StreamPart, {
127
+ type: 'stream-start';
128
+ }>['warnings'];
129
+ /** The fields of a `response-metadata` part, without its `type` tag. */
130
+ type ResponseMetadata = Omit<Extract<LanguageModelV3StreamPart, {
131
+ type: 'response-metadata';
132
+ }>, 'type'>;
133
+ /** Options for the streamed-text part builders: a stable part `id` plus a tokenization strategy. */
134
+ type StreamPartOptions = {
135
+ /** Stable id shared by the start/delta/end parts. */id?: string; /** Split the text into fixed-size slices of at most this many characters. */
136
+ length?: number; /** Split the text on this delimiter, re-appending it to each token. */
137
+ separator?: string;
138
+ };
139
+ /**
140
+ * Builders for individual stream parts emitted by a language model's `doStream`. The text-like
141
+ * builders return a start / delta / end block (without a trailing `finish`) so streams compose by
142
+ * concatenation; control parts (`finish`, `error`, …) are single parts.
143
+ */
144
+ declare const StreamParts: {
145
+ /** A text block: `text-start` → `text-delta`* → `text-end`. */text: (text: string, {
146
+ id,
147
+ length,
148
+ separator
149
+ }?: StreamPartOptions) => Array<LanguageModelV3StreamPart>; /** A reasoning block: `reasoning-start` → `reasoning-delta`* → `reasoning-end`. */
150
+ reasoning: (text: string, {
151
+ id,
152
+ length,
153
+ separator
154
+ }?: StreamPartOptions) => Array<LanguageModelV3StreamPart>; /** A streamed tool input: `tool-input-start` → `tool-input-delta`* → `tool-input-end`. */
155
+ toolInput: (args: {
156
+ id: string;
157
+ toolName: string;
158
+ input: unknown;
159
+ length?: number;
160
+ }) => Array<LanguageModelV3StreamPart>; /** A completed tool call (same shape as the content part). */
161
+ toolCall: (args: {
162
+ toolCallId: string;
163
+ toolName: string;
164
+ input: unknown;
165
+ }) => LanguageModelV3StreamPart; /** A tool result (same shape as the content part). */
166
+ toolResult: (args: {
167
+ toolCallId: string;
168
+ toolName: string;
169
+ result: Parameters<typeof Content.toolResult>[0]["result"];
170
+ isError?: boolean;
171
+ }) => LanguageModelV3StreamPart; /** A source part. */
172
+ source: (args: {
173
+ id: string;
174
+ url: string;
175
+ title?: string;
176
+ }) => LanguageModelV3StreamPart; /** A file part. */
177
+ file: (args: {
178
+ mediaType: string;
179
+ data: string | Uint8Array;
180
+ }) => LanguageModelV3StreamPart; /** The terminal `finish` part with usage and finish reason. The finish reason may be a unified string. */
181
+ finish: (opts?: {
182
+ finishReason?: LanguageModelV3FinishReason | LanguageModelV3FinishReason["unified"];
183
+ usage?: LanguageModelV3Usage;
184
+ }) => LanguageModelV3StreamPart; /** An error part, mirroring a provider failing mid-stream. */
185
+ error: (error: unknown) => LanguageModelV3StreamPart; /** The opening `stream-start` part carrying call warnings. */
186
+ streamStart: (warnings?: StreamStartWarnings) => LanguageModelV3StreamPart; /** Provider response metadata (id, timestamp, modelId, …). */
187
+ responseMetadata: (meta?: ResponseMetadata) => LanguageModelV3StreamPart; /** A raw passthrough part. */
188
+ raw: (rawValue: unknown) => LanguageModelV3StreamPart;
189
+ };
190
+ //#endregion
191
+ //#region src/language/options.d.ts
192
+ /**
193
+ * Determinism helpers to spread into `generateText`/`streamText` so ids and timestamps are stable.
194
+ *
195
+ * Note: `Options.stream` sets `_internal.now` to control timestamps, but the AI SDK has a bug where
196
+ * the `finish-step` response timestamp in the error streaming path uses `new Date()` directly. Tests
197
+ * that hit that path additionally need `vi.useFakeTimers()`.
198
+ *
199
+ * @example
200
+ * await generateText({ model, prompt: 'x', ...Options.generate });
201
+ * streamText({ model, prompt: 'x', ...Options.stream });
202
+ */
203
+ declare const Options: {
204
+ /** Stable id generator used by `Options.generate` / `Options.stream`. */generateId: () => string; /** Spread into `generateText` for a deterministic `generateId`. */
205
+ generate: {
206
+ _internal: {
207
+ generateId: () => string;
208
+ };
209
+ }; /** Spread into `streamText` for a deterministic `generateId` and `now`. */
210
+ stream: {
211
+ _internal: {
212
+ generateId: () => string;
213
+ now: () => number;
214
+ };
215
+ };
216
+ };
217
+ //#endregion
218
+ export { Content, type GenerateResponse, MockLanguageModel, type MockLanguageModelOptions, type MockResponse, Options, Stream, type StreamDelayOptions, type StreamPartOptions, StreamParts, type StreamResponse };
@@ -0,0 +1,363 @@
1
+ import { t as tokenize } from "../tokenize-C-Zp26iY.mjs";
2
+ import { simulateReadableStream } from "ai";
3
+ import { vi } from "vitest";
4
+ import { convertArrayToReadableStream, convertReadableStreamToArray } from "@ai-sdk/provider-utils/test";
5
+
6
+ //#region src/internal/defaults.ts
7
+ /** Standard "stop" finish reason used when none is supplied. */
8
+ const defaultFinishReason = {
9
+ unified: "stop",
10
+ raw: "stop"
11
+ };
12
+ /** Small, stable token usage used when none is supplied. */
13
+ const defaultUsage = {
14
+ inputTokens: {
15
+ total: 10,
16
+ noCache: 10,
17
+ cacheRead: 0,
18
+ cacheWrite: 0
19
+ },
20
+ outputTokens: {
21
+ total: 20,
22
+ text: 20,
23
+ reasoning: 0
24
+ }
25
+ };
26
+ /** Normalizes a finish reason: a bare unified value becomes `{ unified, raw }`; an object passes through. */
27
+ const toFinishReason = (reason) => typeof reason === "string" ? {
28
+ unified: reason,
29
+ raw: reason
30
+ } : reason;
31
+
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
+ //#endregion
38
+ //#region src/language/content.ts
39
+ /** Builders for the static content parts a language model returns from `doGenerate`. */
40
+ const Content = {
41
+ text: (text) => ({
42
+ type: "text",
43
+ text
44
+ }),
45
+ reasoning: (text) => ({
46
+ type: "reasoning",
47
+ text
48
+ }),
49
+ toolCall: (args) => ({
50
+ type: "tool-call",
51
+ toolCallId: args.toolCallId,
52
+ toolName: args.toolName,
53
+ input: toJSONString(args.input)
54
+ }),
55
+ toolResult: (args) => ({
56
+ type: "tool-result",
57
+ toolCallId: args.toolCallId,
58
+ toolName: args.toolName,
59
+ result: args.result,
60
+ ...args.isError !== void 0 ? { isError: args.isError } : {}
61
+ }),
62
+ file: (args) => ({
63
+ type: "file",
64
+ mediaType: args.mediaType,
65
+ data: args.data
66
+ }),
67
+ source: (args) => ({
68
+ type: "source",
69
+ sourceType: "url",
70
+ id: args.id,
71
+ url: args.url,
72
+ ...args.title !== void 0 ? { title: args.title } : {}
73
+ })
74
+ };
75
+
76
+ //#endregion
77
+ //#region src/language/stream.ts
78
+ /** Operations for building, draining, and inspecting language model streams in tests. */
79
+ const Stream = {
80
+ from: (parts) => convertArrayToReadableStream(parts),
81
+ simulate: (chunks, opts = {}) => simulateReadableStream({
82
+ chunks,
83
+ ...opts
84
+ }),
85
+ toArray: (stream) => convertReadableStreamToArray(stream),
86
+ text: (parts) => parts.filter((part) => part.type === "text-delta").map((part) => part.delta).join(""),
87
+ finishReason: (parts) => parts.find((part) => part.type === "finish")?.finishReason
88
+ };
89
+
90
+ //#endregion
91
+ //#region src/language/stream-parts.ts
92
+ /**
93
+ * Builders for individual stream parts emitted by a language model's `doStream`. The text-like
94
+ * builders return a start / delta / end block (without a trailing `finish`) so streams compose by
95
+ * concatenation; control parts (`finish`, `error`, …) are single parts.
96
+ */
97
+ const StreamParts = {
98
+ text: (text, { id = "1", length, separator } = {}) => [
99
+ {
100
+ type: "text-start",
101
+ id
102
+ },
103
+ ...tokenize(text, {
104
+ length,
105
+ separator
106
+ }).map((delta) => ({
107
+ type: "text-delta",
108
+ id,
109
+ delta
110
+ })),
111
+ {
112
+ type: "text-end",
113
+ id
114
+ }
115
+ ],
116
+ reasoning: (text, { id = "1", length, separator } = {}) => [
117
+ {
118
+ type: "reasoning-start",
119
+ id
120
+ },
121
+ ...tokenize(text, {
122
+ length,
123
+ separator
124
+ }).map((delta) => ({
125
+ type: "reasoning-delta",
126
+ id,
127
+ delta
128
+ })),
129
+ {
130
+ type: "reasoning-end",
131
+ id
132
+ }
133
+ ],
134
+ toolInput: (args) => [
135
+ {
136
+ type: "tool-input-start",
137
+ id: args.id,
138
+ toolName: args.toolName
139
+ },
140
+ ...tokenize(toJSONString(args.input), { length: args.length }).map((delta) => ({
141
+ type: "tool-input-delta",
142
+ id: args.id,
143
+ delta
144
+ })),
145
+ {
146
+ type: "tool-input-end",
147
+ id: args.id
148
+ }
149
+ ],
150
+ toolCall: (args) => Content.toolCall(args),
151
+ toolResult: (args) => Content.toolResult(args),
152
+ source: (args) => Content.source(args),
153
+ file: (args) => Content.file(args),
154
+ finish: (opts = {}) => ({
155
+ type: "finish",
156
+ finishReason: toFinishReason(opts.finishReason ?? defaultFinishReason),
157
+ usage: opts.usage ?? defaultUsage
158
+ }),
159
+ error: (error) => ({
160
+ type: "error",
161
+ error
162
+ }),
163
+ streamStart: (warnings = []) => ({
164
+ type: "stream-start",
165
+ warnings
166
+ }),
167
+ responseMetadata: (meta = {}) => ({
168
+ type: "response-metadata",
169
+ ...meta
170
+ }),
171
+ raw: (rawValue) => ({
172
+ type: "raw",
173
+ rawValue
174
+ })
175
+ };
176
+
177
+ //#endregion
178
+ //#region src/language/mock-language-model.ts
179
+ /** Monotonic counter backing the auto-generated model ids. */
180
+ let modelCounter = 0;
181
+ /** Returns the next unique auto-generated model id. */
182
+ const nextModelId = () => {
183
+ modelCounter += 1;
184
+ return `mock-model-${modelCounter}`;
185
+ };
186
+ /** Throws a clear error when a method is called but no matching response was configured. */
187
+ const notImplemented = (method) => {
188
+ throw new Error(`MockLanguageModel.${method} was called but no matching response was provided.`);
189
+ };
190
+ /** Narrows a response to the explicit `{ generate, stream }` form. */
191
+ const isExplicit = (response) => typeof response === "object" && response !== null && !(response instanceof Error) && ("generate" in response || "stream" in response);
192
+ /** Expands a single content part into the stream parts that represent it. */
193
+ const partToStreamParts = (part, id) => {
194
+ if (part.type === "text") return StreamParts.text(part.text, { id });
195
+ if (part.type === "reasoning") return StreamParts.reasoning(part.text, { id });
196
+ return [part];
197
+ };
198
+ /** Derives a stream from content parts: `stream-start` → one block per part → `finish`. */
199
+ const contentToStream = (content, finishReason, usage) => [
200
+ StreamParts.streamStart(),
201
+ ...content.flatMap((part, index) => partToStreamParts(part, String(index))),
202
+ StreamParts.finish({
203
+ finishReason,
204
+ usage
205
+ })
206
+ ];
207
+ /** The streamed form of a string is the streamed form of a single text content part. */
208
+ const textToStream = (text) => contentToStream([Content.text(text)]);
209
+ /** Fills a partial generate result with default finish reason, usage, and warnings. */
210
+ const buildGenerateResult = (input) => ({
211
+ finishReason: defaultFinishReason,
212
+ usage: defaultUsage,
213
+ warnings: [],
214
+ ...input
215
+ });
216
+ /** Wraps stream parts into a stream result, with optional simulated delays. */
217
+ const buildStreamResult = (chunks, initialDelayInMs, chunkDelayInMs) => ({ stream: simulateReadableStream({
218
+ chunks,
219
+ initialDelayInMs,
220
+ chunkDelayInMs
221
+ }) });
222
+ /** Resolves the `generate` form of an explicit response into a generate result. */
223
+ const resolveGenerateResponse = async (response, options) => {
224
+ if (typeof response === "string") return buildGenerateResult({ content: [Content.text(response)] });
225
+ if (response instanceof Error) throw response;
226
+ if (typeof response === "function") return response(options);
227
+ return buildGenerateResult(response);
228
+ };
229
+ /** Resolves the `stream` form of an explicit response into a stream result. */
230
+ const resolveStreamResponse = async (response, options) => {
231
+ if (typeof response === "string") return buildStreamResult(textToStream(response));
232
+ if (response instanceof Error) throw response;
233
+ if (Array.isArray(response)) return buildStreamResult(response);
234
+ if (typeof response === "function") return response(options);
235
+ if ("chunks" in response) return buildStreamResult(response.chunks, response.initialDelayInMs, response.chunkDelayInMs);
236
+ return response;
237
+ };
238
+ /** Resolves a top-level response for a `doGenerate` call. */
239
+ const resolveGenerate = async (response, options) => {
240
+ if (typeof response === "string") return buildGenerateResult({ content: [Content.text(response)] });
241
+ if (response instanceof Error) throw response;
242
+ if (isExplicit(response)) return response.generate === void 0 ? notImplemented("doGenerate") : resolveGenerateResponse(response.generate, options);
243
+ if ("content" in response) return buildGenerateResult(response);
244
+ return notImplemented("doGenerate");
245
+ };
246
+ /** Resolves a top-level response for a `doStream` call. */
247
+ const resolveStream = async (response, options) => {
248
+ if (typeof response === "string") return buildStreamResult(textToStream(response));
249
+ if (response instanceof Error) throw response;
250
+ 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));
252
+ return notImplemented("doStream");
253
+ };
254
+ /** Picks the response for the current call: a single response repeats, an array advances and clamps. */
255
+ const pickResponse = (input, callIndex) => {
256
+ if (!Array.isArray(input)) return input;
257
+ if (input.length === 0) return {};
258
+ return input[Math.min(callIndex, input.length - 1)] ?? {};
259
+ };
260
+ /**
261
+ * A `LanguageModelV3` mock whose `doGenerate`/`doStream` are `vi.fn()` spies. Each call is also
262
+ * recorded on `doGenerateCalls`/`doStreamCalls` so call arguments can be inspected without vitest.
263
+ * Instances are created via the {@link MockLanguageModel} factory.
264
+ */
265
+ var LanguageModelMock = class {
266
+ /** The language model spec version this mock implements. */
267
+ specificationVersion = "v3";
268
+ /** URL patterns the model supports — none, for a mock. */
269
+ supportedUrls = {};
270
+ /** The provider id. */
271
+ provider;
272
+ /** The model id. */
273
+ modelId;
274
+ /** Spy implementing `doGenerate`, resolving the configured response. */
275
+ doGenerate;
276
+ /** Spy implementing `doStream`, resolving the configured response. */
277
+ doStream;
278
+ /** Call options captured for every `doGenerate` invocation, in order. */
279
+ doGenerateCalls = [];
280
+ /** Call options captured for every `doStream` invocation, in order. */
281
+ doStreamCalls = [];
282
+ /** Builds the spies and identity from the configured response(s) and options. */
283
+ constructor(input = {}, options = {}) {
284
+ this.provider = options.provider ?? "mock-provider";
285
+ this.modelId = options.modelId ?? nextModelId();
286
+ this.doGenerate = vi.fn(async (callOptions) => {
287
+ const response = pickResponse(input, this.doGenerateCalls.length);
288
+ this.doGenerateCalls.push(callOptions);
289
+ return resolveGenerate(response, callOptions);
290
+ });
291
+ this.doStream = vi.fn(async (callOptions) => {
292
+ const response = pickResponse(input, this.doStreamCalls.length);
293
+ this.doStreamCalls.push(callOptions);
294
+ return resolveStream(response, callOptions);
295
+ });
296
+ }
297
+ };
298
+ /** Builds the content array for a generate result: a string becomes one text part; an array passes through. */
299
+ const content = (input) => typeof input === "string" ? [Content.text(input)] : input;
300
+ /** Builds a full generate result, filling finish reason, usage, and warnings. */
301
+ const generateResult = (input) => buildGenerateResult(typeof input === "string" ? { content: [Content.text(input)] } : input);
302
+ /** Builds a full stream result; a string is assembled into `stream-start` → text → `finish`. */
303
+ const streamResult = (input, opts = {}) => buildStreamResult(typeof input === "string" ? textToStream(input) : input, opts.initialDelayInMs, opts.chunkDelayInMs);
304
+ /** Builds a usage object, overriding individual token fields on top of the defaults. */
305
+ const usage = (overrides = {}) => ({
306
+ inputTokens: {
307
+ ...defaultUsage.inputTokens,
308
+ ...overrides.inputTokens
309
+ },
310
+ outputTokens: {
311
+ ...defaultUsage.outputTokens,
312
+ ...overrides.outputTokens
313
+ }
314
+ });
315
+ /** Builds a finish reason from its unified value (raw mirrors it). */
316
+ const finishReason = (unified = "stop") => toFinishReason(unified);
317
+ /** Creates a mock `LanguageModelV3` from a response spec (or sequence of them). */
318
+ const from = (input, options) => new LanguageModelMock(input ?? {}, options);
319
+ /**
320
+ * Namespace for building mock language models. `from` creates a mock `LanguageModelV3`; the other
321
+ * builders assemble the values a model returns. Exported as both a value (the namespace) and a type
322
+ * (the model instance).
323
+ *
324
+ * @example
325
+ * const model = MockLanguageModel.from('Hello, world!');
326
+ * const flaky = MockLanguageModel.from([new Error('rate limited'), 'recovered']);
327
+ * const built = MockLanguageModel.from({ content: MockLanguageModel.content('Hi') });
328
+ */
329
+ const MockLanguageModel = {
330
+ from,
331
+ content,
332
+ generateResult,
333
+ streamResult,
334
+ usage,
335
+ finishReason
336
+ };
337
+
338
+ //#endregion
339
+ //#region src/language/options.ts
340
+ /** Deterministic id generator so generated message ids are stable across runs. */
341
+ const generateId = () => "aitxt-mock-id";
342
+ /**
343
+ * Determinism helpers to spread into `generateText`/`streamText` so ids and timestamps are stable.
344
+ *
345
+ * Note: `Options.stream` sets `_internal.now` to control timestamps, but the AI SDK has a bug where
346
+ * the `finish-step` response timestamp in the error streaming path uses `new Date()` directly. Tests
347
+ * that hit that path additionally need `vi.useFakeTimers()`.
348
+ *
349
+ * @example
350
+ * await generateText({ model, prompt: 'x', ...Options.generate });
351
+ * streamText({ model, prompt: 'x', ...Options.stream });
352
+ */
353
+ const Options = {
354
+ generateId,
355
+ generate: { _internal: { generateId } },
356
+ stream: { _internal: {
357
+ generateId,
358
+ now: () => 0
359
+ } }
360
+ };
361
+
362
+ //#endregion
363
+ export { Content, MockLanguageModel, Options, Stream, StreamParts };
@@ -0,0 +1,13 @@
1
+ //#region src/internal/tokenize.ts
2
+ /**
3
+ * Splits text into tokens. Shared by every streamed-text builder so chunking behavior lives
4
+ * in one place. With no strategy the whole string is a single token.
5
+ */
6
+ const tokenize = (text, { length, separator } = {}) => {
7
+ if (separator !== void 0) return text.split(separator).map((token) => token + separator);
8
+ if (length !== void 0) return text.split(new RegExp(`(.{1,${length}})`)).filter((token) => token.length > 0);
9
+ return [text];
10
+ };
11
+
12
+ //#endregion
13
+ export { tokenize as t };