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.
- package/LICENSE +21 -0
- package/README.md +924 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/language/index.d.mts +218 -0
- package/dist/language/index.mjs +363 -0
- package/dist/tokenize-C-Zp26iY.mjs +13 -0
- package/dist/ui/index.d.mts +2302 -0
- package/dist/ui/index.mjs +266 -0
- package/package.json +71 -0
package/dist/index.d.mts
ADDED
|
@@ -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 };
|