@nemesis-oss/ollama-sdk 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,336 @@
1
+ /**
2
+ * In-memory Ollama mock server for deterministic, zero-network CI testing.
3
+ *
4
+ * This is the SDK's own mock engine, promoted from the internal test fixture
5
+ * (`test/mocks/ollama-mock-server.ts`) to a public entry point
6
+ * (`@nemesis-oss/ollama-sdk/testing`) so consumers don't have to hand-roll
7
+ * ad-hoc HTTP stubs to test their Ollama integrations — see ADR 0029.
8
+ * The internal fixture re-exports this module, so the SDK's own test suite
9
+ * exercises the exact same code consumers get.
10
+ *
11
+ * The VCR cassette system (`test/vcr.ts` in the repo) replays recorded HTTP
12
+ * interactions, but cassettes are static. This mock server covers what
13
+ * cassettes can't:
14
+ *
15
+ * - programmable inter-chunk delays (backpressure handling)
16
+ * - mid-stream **in-band error injection** — an `{"error": "..."}` NDJSON
17
+ * frame emitted mid-response with HTTP 200 still, exactly how a real
18
+ * Ollama daemon reports GPU OOM / driver crashes mid-generation
19
+ * - connection drops mid-stream (`dropConnectionMidStream`)
20
+ * - 503/502/429 error status codes with custom bodies
21
+ * - arbitrary NDJSON chunk payloads (tool calls, thinking, partial frames)
22
+ *
23
+ * On top of the raw engine, the public surface adds the conveniences a
24
+ * consumer test suite wants on day one:
25
+ *
26
+ * - {@link createMockOllamaServer} factory (the recommended entry point)
27
+ * - built-in **default routes** — the official liveness probe (`HEAD /`
28
+ * and `GET /` → "Ollama is running") and `GET /api/version` — so
29
+ * `client.ping()` and friends work before any route is registered.
30
+ * Explicit `register()` calls always win.
31
+ * - **request recording** (`server.requests`, `clearRequests()`) for
32
+ * wire-level assertions without per-route callbacks
33
+ * - NDJSON **chunk builders** ({@link mockChatChunk}, {@link mockTextChunks},
34
+ * {@link mockToolCallChunks}) that produce correctly-shaped `/api/chat`
35
+ * frames — including parallel tool calls in one frame, the documented
36
+ * native behavior
37
+ *
38
+ * ## Usage
39
+ *
40
+ * ```ts
41
+ * import { createMockOllamaServer, mockToolCallChunks } from '@nemesis-oss/ollama-sdk/testing';
42
+ * import { OllamaClient } from '@nemesis-oss/ollama-sdk';
43
+ *
44
+ * const server = createMockOllamaServer(); // ephemeral port, default routes on
45
+ * await server.start();
46
+ *
47
+ * server.register('/api/chat', {
48
+ * chunks: mockToolCallChunks([
49
+ * { name: 'get_weather', arguments: { city: 'Tokyo' } },
50
+ * { name: 'get_time', arguments: { tz: 'Asia/Tokyo' } },
51
+ * ]),
52
+ * });
53
+ *
54
+ * const client = new OllamaClient({ baseUrl: server.baseUrl });
55
+ * // ... exercise the client; assert on server.requests ...
56
+ *
57
+ * await server.stop();
58
+ * ```
59
+ *
60
+ * ## Environment
61
+ *
62
+ * Node.js only — the module builds on `node:http`, which is why it lives in
63
+ * a dedicated subpath export: importing it from the browser/Edge-safe main
64
+ * entry would drag a Node built-in into every bundle. Never re-export this
65
+ * module from `src/index.ts`; the Edge Runtime verification
66
+ * (`npm run verify:edge-runtime`) exists to catch exactly that.
67
+ *
68
+ * Designed for Vitest/Jest: `start()` resolves once the server listens and
69
+ * `stop()` is idempotent, so `beforeEach`/`afterEach` hooks manage the
70
+ * lifecycle without tracking whether `start()` ran.
71
+ */
72
+ /** One recorded incoming request, for wire-level test assertions. */
73
+ interface MockRequestRecord {
74
+ /** HTTP method (`GET`, `POST`, `HEAD`, ...). */
75
+ readonly method: string;
76
+ /** Request path with the query string stripped. */
77
+ readonly path: string;
78
+ /** Parsed request body (JSON-parsed when possible, raw string otherwise); `undefined` for body-less methods. */
79
+ readonly body?: unknown;
80
+ /** ISO-8601 timestamp of when the request arrived. */
81
+ readonly timestamp: string;
82
+ }
83
+ /**
84
+ * A single mock route handler. The server matches on the request path
85
+ * (query string stripped — Ollama's API doesn't use query params for the
86
+ * routes we care about).
87
+ *
88
+ * All fields optional except `chunks` (which defaults to `[]` — useful for
89
+ * testing the "stream closed without any data" case).
90
+ */
91
+ interface MockEndpointOptions {
92
+ /** HTTP status code. Defaults to 200. */
93
+ readonly status?: number | undefined;
94
+ /**
95
+ * NDJSON chunks to emit, in order. Each chunk is emitted as a separate
96
+ * `res.write()` call followed by `\n`. Pass `[]` to emit no body (useful
97
+ * for testing the "stream closed without data" case).
98
+ */
99
+ readonly chunks?: readonly string[] | undefined;
100
+ /**
101
+ * Delay (ms) before emitting each chunk. Defaults to 0 (no delay). Set to
102
+ * a small value (5-50ms) to test backpressure handling; set to a large
103
+ * value (10_000ms) to test timeout/cancellation.
104
+ */
105
+ readonly chunkDelayMs?: number | undefined;
106
+ /**
107
+ * Custom response headers. Defaults to
108
+ * `{ 'content-type': 'application/x-ndjson' }` for streaming endpoints.
109
+ * Override for non-JSON responses (e.g. blob uploads).
110
+ */
111
+ readonly headers?: Record<string, string> | undefined;
112
+ /**
113
+ * When `true`, the server closes the connection mid-stream after emitting
114
+ * the first chunk. Use this to test connection-drop recovery. Defaults to
115
+ * `false`.
116
+ */
117
+ readonly dropConnectionMidStream?: boolean | undefined;
118
+ /**
119
+ * Emit this exact string as the whole response body — no NDJSON newline
120
+ * framing. Overrides `chunks` (use it for non-JSON responses such as the
121
+ * plain-text liveness body or raw blob bytes).
122
+ */
123
+ readonly rawBody?: string | undefined;
124
+ /**
125
+ * Inject an in-band error frame — `{"error": "<message>"}` — into the
126
+ * NDJSON response while the HTTP status stays 200. This is how a real
127
+ * Ollama daemon reports mid-generation failures (GPU OOM, driver crash,
128
+ * context overflow): the status code never changes, the parser has to
129
+ * catch the frame. The SDK's stream readers raise `OllamaStreamError` on
130
+ * it.
131
+ *
132
+ * `after` is the number of chunks emitted before the error frame. The
133
+ * default is `chunks.length - 1` — every content chunk, then the error
134
+ * frame, with the final `done` frame never emitted, exactly what a
135
+ * mid-generation crash looks like on the wire (an error frame *after* the
136
+ * done frame would be unread: the stream has already completed cleanly).
137
+ * `after: 0` errors before any content; an explicit `after >=
138
+ * chunks.length` emits every chunk and then the error. Chunks at or after
139
+ * the injection point are never emitted: the server "crashed".
140
+ */
141
+ readonly injectErrorFrame?: {
142
+ readonly after?: number | undefined;
143
+ readonly message: string;
144
+ } | undefined;
145
+ /**
146
+ * Optional callback fired when a request arrives at this route. Receives
147
+ * the parsed request body (or `undefined` if the body wasn't valid JSON).
148
+ * Useful for assertions like "the client sent the right `model` field".
149
+ * For global capture across routes see {@link OllamaMockServer.requests}.
150
+ */
151
+ readonly onRequest?: ((body: unknown) => void) | undefined;
152
+ }
153
+ /** Options for {@link createMockOllamaServer} and the `OllamaMockServer` constructor. */
154
+ interface MockServerOptions {
155
+ /** TCP port to listen on. `0` (the default) picks an ephemeral port. */
156
+ readonly port?: number | undefined;
157
+ /**
158
+ * Serve the built-in default routes when no route is explicitly registered
159
+ * for a path: `HEAD /` and `GET /` → `200` (the official liveness probe —
160
+ * `GET /` answers the plain-text body "Ollama is running") and
161
+ * `GET /api/version` → `200 {"version": "<version>"}`. Defaults to `true`.
162
+ * Explicit `register()` calls always take precedence.
163
+ */
164
+ readonly defaultRoutes?: boolean | undefined;
165
+ /** Version reported by the default `GET /api/version` route. Defaults to `'0.14.0'`. */
166
+ readonly version?: string | undefined;
167
+ }
168
+ /** Shape accepted by {@link mockToolCallChunks} for each tool call. */
169
+ interface MockToolCallSpec {
170
+ /** Tool-call id; auto-generated (`call-0`, `call-1`, …) when omitted. */
171
+ readonly id?: string | undefined;
172
+ /** Tool name, as the model would emit it. */
173
+ readonly name: string;
174
+ /** Tool arguments — an object (stringified by the builder) or a pre-encoded string. */
175
+ readonly arguments: Record<string, unknown> | string;
176
+ }
177
+ /** Options for {@link mockTextChunks}. */
178
+ interface MockTextChunksOptions {
179
+ /** Split the text into content frames of at most this many characters. Default: one frame with the whole text. */
180
+ readonly chunkSize?: number | undefined;
181
+ /** Model name stamped on each frame. Defaults to `'mock-model'`. */
182
+ readonly model?: string | undefined;
183
+ }
184
+ /** Options for {@link mockToolCallChunks}. */
185
+ interface MockToolCallChunksOptions {
186
+ /** Model name stamped on each frame. Defaults to `'mock-model'`. */
187
+ readonly model?: string | undefined;
188
+ /** Assistant text to carry on the tool-call frame (some models emit a preamble). Defaults to `''`. */
189
+ readonly content?: string | undefined;
190
+ }
191
+ /**
192
+ * One `/api/chat` NDJSON frame as a ready-to-emit string. Sensible defaults:
193
+ * `model: 'mock-model'`, a fresh `created_at`, `done: false`.
194
+ *
195
+ * ```ts
196
+ * server.register('/api/chat', {
197
+ * chunks: [
198
+ * mockChatChunk({ content: 'Hel' }),
199
+ * mockChatChunk({ content: 'lo' }),
200
+ * mockChatChunk({ done: true, evalCount: 2, evalDurationNs: 20_000_000 }),
201
+ * ],
202
+ * });
203
+ * ```
204
+ */
205
+ declare function mockChatChunk(frame: {
206
+ readonly content?: string | undefined;
207
+ readonly toolCalls?: readonly MockToolCallSpec[] | undefined;
208
+ readonly done?: boolean | undefined;
209
+ readonly model?: string | undefined;
210
+ readonly evalCount?: number | undefined;
211
+ readonly evalDurationNs?: number | undefined;
212
+ readonly promptEvalCount?: number | undefined;
213
+ readonly promptEvalDurationNs?: number | undefined;
214
+ readonly promptEvalCachedCount?: number | undefined;
215
+ readonly totalDurationNs?: number | undefined;
216
+ readonly loadDurationNs?: number | undefined;
217
+ }): string;
218
+ /**
219
+ * Content frames + a final `done` frame for a plain-text `/api/chat` reply.
220
+ * With `chunkSize`, the text is split into that many characters per frame —
221
+ * the shape streaming code needs to exercise incremental assembly.
222
+ *
223
+ * ```ts
224
+ * server.register('/api/chat', {
225
+ * chunks: mockTextChunks('Hello, world!', { chunkSize: 5 }),
226
+ * });
227
+ * ```
228
+ */
229
+ declare function mockTextChunks(text: string, opts?: MockTextChunksOptions): string[];
230
+ /**
231
+ * Tool-call frames for `/api/chat` — the documented native shape: tool calls
232
+ * arrive **complete in one frame** (parallel calls = one array with several
233
+ * entries), followed by a final `done` frame. Pair with an `Agent` or tool
234
+ * loop for deterministic tool-execution tests.
235
+ *
236
+ * ```ts
237
+ * // Parallel tool calls, one round trip:
238
+ * server.register('/api/chat', {
239
+ * chunks: mockToolCallChunks([
240
+ * { name: 'get_weather', arguments: { city: 'Tokyo' } },
241
+ * { name: 'get_time', arguments: { tz: 'Asia/Tokyo' } },
242
+ * ]),
243
+ * });
244
+ * ```
245
+ */
246
+ declare function mockToolCallChunks(toolCalls: readonly MockToolCallSpec[], opts?: MockToolCallChunksOptions): string[];
247
+ /**
248
+ * Lightweight in-memory Ollama mock server.
249
+ *
250
+ * Spawns a real `node:http` server on a free port (or a fixed port if you
251
+ * pass one). Tests register route handlers via `register()`, then point an
252
+ * `OllamaClient` at `server.baseUrl`. The server emits the registered chunks
253
+ * with the configured delay, status, and connection behavior; when no route
254
+ * is registered the built-in liveness + version defaults answer (see
255
+ * {@link MockServerOptions.defaultRoutes}).
256
+ *
257
+ * Prefer the {@link createMockOllamaServer} factory; the class is exported
258
+ * for typed references and back-compatibility with the SDK's own suite.
259
+ *
260
+ * The server is **single-process** — it doesn't fork or use workers. It's
261
+ * intended for unit/integration tests, not load testing.
262
+ */
263
+ declare class OllamaMockServer {
264
+ private readonly server;
265
+ private readonly routes;
266
+ private readonly requestLog;
267
+ private readonly defaultsEnabled;
268
+ private readonly defaultVersion;
269
+ private port;
270
+ /**
271
+ * @param portOrOptions Either a TCP port (pass `0` for an ephemeral port —
272
+ * recommended for parallel test runs) or a {@link MockServerOptions}
273
+ * object. The actual port is exposed via {@link baseUrl} after
274
+ * {@link start}.
275
+ */
276
+ constructor(portOrOptions?: number | MockServerOptions);
277
+ /**
278
+ * Register a route handler. Replaces any existing handler for the same
279
+ * path (including a built-in default). The path is matched against
280
+ * `req.url` exactly (query string stripped, no template matching — for
281
+ * `/api/blobs/{digest}`, register each specific digest value).
282
+ */
283
+ register(path: string, options: MockEndpointOptions): void;
284
+ /** Clear all registered routes (built-in defaults remain). Useful in `beforeEach` hooks. */
285
+ clear(): void;
286
+ /**
287
+ * Every request this server has served, in arrival order — method, path,
288
+ * parsed body, timestamp — including 404s and default-route hits. Assert
289
+ * on what the client actually put on the wire; clear with
290
+ * {@link clearRequests} (e.g. in `beforeEach`).
291
+ */
292
+ get requests(): readonly MockRequestRecord[];
293
+ /** Empty the request log ({@link requests}). Routes are unaffected. */
294
+ clearRequests(): void;
295
+ /**
296
+ * Base URL to point an `OllamaClient` at. Only meaningful after
297
+ * {@link start} has resolved.
298
+ */
299
+ get baseUrl(): string;
300
+ /**
301
+ * Start listening. Resolves once the server is ready to accept
302
+ * connections.
303
+ */
304
+ start(): Promise<void>;
305
+ /**
306
+ * Stop listening and close all active connections. Resolves once the
307
+ * server is fully closed (no lingering sockets). Idempotent — calling
308
+ * `stop()` on an already-stopped server resolves cleanly rather than
309
+ * throwing `ERR_SERVER_NOT_RUNNING`.
310
+ */
311
+ stop(): Promise<void>;
312
+ /**
313
+ * Built-in default handler for a path, or `undefined` when defaults are
314
+ * disabled or the path isn't one of the official liveness surfaces.
315
+ * Registered routes are checked first and always win.
316
+ */
317
+ private defaultRouteFor;
318
+ private handleRequest;
319
+ }
320
+ /**
321
+ * Create an in-memory Ollama mock server — the recommended entry point for
322
+ * consumer test suites. Ephemeral port, built-in liveness + version
323
+ * defaults on, empty request log. Call `await server.start()` before
324
+ * pointing an `OllamaClient` at `server.baseUrl`, and `await server.stop()`
325
+ * in teardown.
326
+ *
327
+ * ```ts
328
+ * const server = createMockOllamaServer();
329
+ * await server.start();
330
+ * // defaults already answer: HEAD / (ping) and GET /api/version
331
+ * server.register('/api/chat', { chunks: mockTextChunks('Hi!') });
332
+ * ```
333
+ */
334
+ declare function createMockOllamaServer(options?: MockServerOptions): OllamaMockServer;
335
+
336
+ export { type MockEndpointOptions, type MockRequestRecord, type MockServerOptions, type MockTextChunksOptions, type MockToolCallChunksOptions, type MockToolCallSpec, OllamaMockServer, createMockOllamaServer, mockChatChunk, mockTextChunks, mockToolCallChunks };
@@ -0,0 +1,336 @@
1
+ /**
2
+ * In-memory Ollama mock server for deterministic, zero-network CI testing.
3
+ *
4
+ * This is the SDK's own mock engine, promoted from the internal test fixture
5
+ * (`test/mocks/ollama-mock-server.ts`) to a public entry point
6
+ * (`@nemesis-oss/ollama-sdk/testing`) so consumers don't have to hand-roll
7
+ * ad-hoc HTTP stubs to test their Ollama integrations — see ADR 0029.
8
+ * The internal fixture re-exports this module, so the SDK's own test suite
9
+ * exercises the exact same code consumers get.
10
+ *
11
+ * The VCR cassette system (`test/vcr.ts` in the repo) replays recorded HTTP
12
+ * interactions, but cassettes are static. This mock server covers what
13
+ * cassettes can't:
14
+ *
15
+ * - programmable inter-chunk delays (backpressure handling)
16
+ * - mid-stream **in-band error injection** — an `{"error": "..."}` NDJSON
17
+ * frame emitted mid-response with HTTP 200 still, exactly how a real
18
+ * Ollama daemon reports GPU OOM / driver crashes mid-generation
19
+ * - connection drops mid-stream (`dropConnectionMidStream`)
20
+ * - 503/502/429 error status codes with custom bodies
21
+ * - arbitrary NDJSON chunk payloads (tool calls, thinking, partial frames)
22
+ *
23
+ * On top of the raw engine, the public surface adds the conveniences a
24
+ * consumer test suite wants on day one:
25
+ *
26
+ * - {@link createMockOllamaServer} factory (the recommended entry point)
27
+ * - built-in **default routes** — the official liveness probe (`HEAD /`
28
+ * and `GET /` → "Ollama is running") and `GET /api/version` — so
29
+ * `client.ping()` and friends work before any route is registered.
30
+ * Explicit `register()` calls always win.
31
+ * - **request recording** (`server.requests`, `clearRequests()`) for
32
+ * wire-level assertions without per-route callbacks
33
+ * - NDJSON **chunk builders** ({@link mockChatChunk}, {@link mockTextChunks},
34
+ * {@link mockToolCallChunks}) that produce correctly-shaped `/api/chat`
35
+ * frames — including parallel tool calls in one frame, the documented
36
+ * native behavior
37
+ *
38
+ * ## Usage
39
+ *
40
+ * ```ts
41
+ * import { createMockOllamaServer, mockToolCallChunks } from '@nemesis-oss/ollama-sdk/testing';
42
+ * import { OllamaClient } from '@nemesis-oss/ollama-sdk';
43
+ *
44
+ * const server = createMockOllamaServer(); // ephemeral port, default routes on
45
+ * await server.start();
46
+ *
47
+ * server.register('/api/chat', {
48
+ * chunks: mockToolCallChunks([
49
+ * { name: 'get_weather', arguments: { city: 'Tokyo' } },
50
+ * { name: 'get_time', arguments: { tz: 'Asia/Tokyo' } },
51
+ * ]),
52
+ * });
53
+ *
54
+ * const client = new OllamaClient({ baseUrl: server.baseUrl });
55
+ * // ... exercise the client; assert on server.requests ...
56
+ *
57
+ * await server.stop();
58
+ * ```
59
+ *
60
+ * ## Environment
61
+ *
62
+ * Node.js only — the module builds on `node:http`, which is why it lives in
63
+ * a dedicated subpath export: importing it from the browser/Edge-safe main
64
+ * entry would drag a Node built-in into every bundle. Never re-export this
65
+ * module from `src/index.ts`; the Edge Runtime verification
66
+ * (`npm run verify:edge-runtime`) exists to catch exactly that.
67
+ *
68
+ * Designed for Vitest/Jest: `start()` resolves once the server listens and
69
+ * `stop()` is idempotent, so `beforeEach`/`afterEach` hooks manage the
70
+ * lifecycle without tracking whether `start()` ran.
71
+ */
72
+ /** One recorded incoming request, for wire-level test assertions. */
73
+ interface MockRequestRecord {
74
+ /** HTTP method (`GET`, `POST`, `HEAD`, ...). */
75
+ readonly method: string;
76
+ /** Request path with the query string stripped. */
77
+ readonly path: string;
78
+ /** Parsed request body (JSON-parsed when possible, raw string otherwise); `undefined` for body-less methods. */
79
+ readonly body?: unknown;
80
+ /** ISO-8601 timestamp of when the request arrived. */
81
+ readonly timestamp: string;
82
+ }
83
+ /**
84
+ * A single mock route handler. The server matches on the request path
85
+ * (query string stripped — Ollama's API doesn't use query params for the
86
+ * routes we care about).
87
+ *
88
+ * All fields optional except `chunks` (which defaults to `[]` — useful for
89
+ * testing the "stream closed without any data" case).
90
+ */
91
+ interface MockEndpointOptions {
92
+ /** HTTP status code. Defaults to 200. */
93
+ readonly status?: number | undefined;
94
+ /**
95
+ * NDJSON chunks to emit, in order. Each chunk is emitted as a separate
96
+ * `res.write()` call followed by `\n`. Pass `[]` to emit no body (useful
97
+ * for testing the "stream closed without data" case).
98
+ */
99
+ readonly chunks?: readonly string[] | undefined;
100
+ /**
101
+ * Delay (ms) before emitting each chunk. Defaults to 0 (no delay). Set to
102
+ * a small value (5-50ms) to test backpressure handling; set to a large
103
+ * value (10_000ms) to test timeout/cancellation.
104
+ */
105
+ readonly chunkDelayMs?: number | undefined;
106
+ /**
107
+ * Custom response headers. Defaults to
108
+ * `{ 'content-type': 'application/x-ndjson' }` for streaming endpoints.
109
+ * Override for non-JSON responses (e.g. blob uploads).
110
+ */
111
+ readonly headers?: Record<string, string> | undefined;
112
+ /**
113
+ * When `true`, the server closes the connection mid-stream after emitting
114
+ * the first chunk. Use this to test connection-drop recovery. Defaults to
115
+ * `false`.
116
+ */
117
+ readonly dropConnectionMidStream?: boolean | undefined;
118
+ /**
119
+ * Emit this exact string as the whole response body — no NDJSON newline
120
+ * framing. Overrides `chunks` (use it for non-JSON responses such as the
121
+ * plain-text liveness body or raw blob bytes).
122
+ */
123
+ readonly rawBody?: string | undefined;
124
+ /**
125
+ * Inject an in-band error frame — `{"error": "<message>"}` — into the
126
+ * NDJSON response while the HTTP status stays 200. This is how a real
127
+ * Ollama daemon reports mid-generation failures (GPU OOM, driver crash,
128
+ * context overflow): the status code never changes, the parser has to
129
+ * catch the frame. The SDK's stream readers raise `OllamaStreamError` on
130
+ * it.
131
+ *
132
+ * `after` is the number of chunks emitted before the error frame. The
133
+ * default is `chunks.length - 1` — every content chunk, then the error
134
+ * frame, with the final `done` frame never emitted, exactly what a
135
+ * mid-generation crash looks like on the wire (an error frame *after* the
136
+ * done frame would be unread: the stream has already completed cleanly).
137
+ * `after: 0` errors before any content; an explicit `after >=
138
+ * chunks.length` emits every chunk and then the error. Chunks at or after
139
+ * the injection point are never emitted: the server "crashed".
140
+ */
141
+ readonly injectErrorFrame?: {
142
+ readonly after?: number | undefined;
143
+ readonly message: string;
144
+ } | undefined;
145
+ /**
146
+ * Optional callback fired when a request arrives at this route. Receives
147
+ * the parsed request body (or `undefined` if the body wasn't valid JSON).
148
+ * Useful for assertions like "the client sent the right `model` field".
149
+ * For global capture across routes see {@link OllamaMockServer.requests}.
150
+ */
151
+ readonly onRequest?: ((body: unknown) => void) | undefined;
152
+ }
153
+ /** Options for {@link createMockOllamaServer} and the `OllamaMockServer` constructor. */
154
+ interface MockServerOptions {
155
+ /** TCP port to listen on. `0` (the default) picks an ephemeral port. */
156
+ readonly port?: number | undefined;
157
+ /**
158
+ * Serve the built-in default routes when no route is explicitly registered
159
+ * for a path: `HEAD /` and `GET /` → `200` (the official liveness probe —
160
+ * `GET /` answers the plain-text body "Ollama is running") and
161
+ * `GET /api/version` → `200 {"version": "<version>"}`. Defaults to `true`.
162
+ * Explicit `register()` calls always take precedence.
163
+ */
164
+ readonly defaultRoutes?: boolean | undefined;
165
+ /** Version reported by the default `GET /api/version` route. Defaults to `'0.14.0'`. */
166
+ readonly version?: string | undefined;
167
+ }
168
+ /** Shape accepted by {@link mockToolCallChunks} for each tool call. */
169
+ interface MockToolCallSpec {
170
+ /** Tool-call id; auto-generated (`call-0`, `call-1`, …) when omitted. */
171
+ readonly id?: string | undefined;
172
+ /** Tool name, as the model would emit it. */
173
+ readonly name: string;
174
+ /** Tool arguments — an object (stringified by the builder) or a pre-encoded string. */
175
+ readonly arguments: Record<string, unknown> | string;
176
+ }
177
+ /** Options for {@link mockTextChunks}. */
178
+ interface MockTextChunksOptions {
179
+ /** Split the text into content frames of at most this many characters. Default: one frame with the whole text. */
180
+ readonly chunkSize?: number | undefined;
181
+ /** Model name stamped on each frame. Defaults to `'mock-model'`. */
182
+ readonly model?: string | undefined;
183
+ }
184
+ /** Options for {@link mockToolCallChunks}. */
185
+ interface MockToolCallChunksOptions {
186
+ /** Model name stamped on each frame. Defaults to `'mock-model'`. */
187
+ readonly model?: string | undefined;
188
+ /** Assistant text to carry on the tool-call frame (some models emit a preamble). Defaults to `''`. */
189
+ readonly content?: string | undefined;
190
+ }
191
+ /**
192
+ * One `/api/chat` NDJSON frame as a ready-to-emit string. Sensible defaults:
193
+ * `model: 'mock-model'`, a fresh `created_at`, `done: false`.
194
+ *
195
+ * ```ts
196
+ * server.register('/api/chat', {
197
+ * chunks: [
198
+ * mockChatChunk({ content: 'Hel' }),
199
+ * mockChatChunk({ content: 'lo' }),
200
+ * mockChatChunk({ done: true, evalCount: 2, evalDurationNs: 20_000_000 }),
201
+ * ],
202
+ * });
203
+ * ```
204
+ */
205
+ declare function mockChatChunk(frame: {
206
+ readonly content?: string | undefined;
207
+ readonly toolCalls?: readonly MockToolCallSpec[] | undefined;
208
+ readonly done?: boolean | undefined;
209
+ readonly model?: string | undefined;
210
+ readonly evalCount?: number | undefined;
211
+ readonly evalDurationNs?: number | undefined;
212
+ readonly promptEvalCount?: number | undefined;
213
+ readonly promptEvalDurationNs?: number | undefined;
214
+ readonly promptEvalCachedCount?: number | undefined;
215
+ readonly totalDurationNs?: number | undefined;
216
+ readonly loadDurationNs?: number | undefined;
217
+ }): string;
218
+ /**
219
+ * Content frames + a final `done` frame for a plain-text `/api/chat` reply.
220
+ * With `chunkSize`, the text is split into that many characters per frame —
221
+ * the shape streaming code needs to exercise incremental assembly.
222
+ *
223
+ * ```ts
224
+ * server.register('/api/chat', {
225
+ * chunks: mockTextChunks('Hello, world!', { chunkSize: 5 }),
226
+ * });
227
+ * ```
228
+ */
229
+ declare function mockTextChunks(text: string, opts?: MockTextChunksOptions): string[];
230
+ /**
231
+ * Tool-call frames for `/api/chat` — the documented native shape: tool calls
232
+ * arrive **complete in one frame** (parallel calls = one array with several
233
+ * entries), followed by a final `done` frame. Pair with an `Agent` or tool
234
+ * loop for deterministic tool-execution tests.
235
+ *
236
+ * ```ts
237
+ * // Parallel tool calls, one round trip:
238
+ * server.register('/api/chat', {
239
+ * chunks: mockToolCallChunks([
240
+ * { name: 'get_weather', arguments: { city: 'Tokyo' } },
241
+ * { name: 'get_time', arguments: { tz: 'Asia/Tokyo' } },
242
+ * ]),
243
+ * });
244
+ * ```
245
+ */
246
+ declare function mockToolCallChunks(toolCalls: readonly MockToolCallSpec[], opts?: MockToolCallChunksOptions): string[];
247
+ /**
248
+ * Lightweight in-memory Ollama mock server.
249
+ *
250
+ * Spawns a real `node:http` server on a free port (or a fixed port if you
251
+ * pass one). Tests register route handlers via `register()`, then point an
252
+ * `OllamaClient` at `server.baseUrl`. The server emits the registered chunks
253
+ * with the configured delay, status, and connection behavior; when no route
254
+ * is registered the built-in liveness + version defaults answer (see
255
+ * {@link MockServerOptions.defaultRoutes}).
256
+ *
257
+ * Prefer the {@link createMockOllamaServer} factory; the class is exported
258
+ * for typed references and back-compatibility with the SDK's own suite.
259
+ *
260
+ * The server is **single-process** — it doesn't fork or use workers. It's
261
+ * intended for unit/integration tests, not load testing.
262
+ */
263
+ declare class OllamaMockServer {
264
+ private readonly server;
265
+ private readonly routes;
266
+ private readonly requestLog;
267
+ private readonly defaultsEnabled;
268
+ private readonly defaultVersion;
269
+ private port;
270
+ /**
271
+ * @param portOrOptions Either a TCP port (pass `0` for an ephemeral port —
272
+ * recommended for parallel test runs) or a {@link MockServerOptions}
273
+ * object. The actual port is exposed via {@link baseUrl} after
274
+ * {@link start}.
275
+ */
276
+ constructor(portOrOptions?: number | MockServerOptions);
277
+ /**
278
+ * Register a route handler. Replaces any existing handler for the same
279
+ * path (including a built-in default). The path is matched against
280
+ * `req.url` exactly (query string stripped, no template matching — for
281
+ * `/api/blobs/{digest}`, register each specific digest value).
282
+ */
283
+ register(path: string, options: MockEndpointOptions): void;
284
+ /** Clear all registered routes (built-in defaults remain). Useful in `beforeEach` hooks. */
285
+ clear(): void;
286
+ /**
287
+ * Every request this server has served, in arrival order — method, path,
288
+ * parsed body, timestamp — including 404s and default-route hits. Assert
289
+ * on what the client actually put on the wire; clear with
290
+ * {@link clearRequests} (e.g. in `beforeEach`).
291
+ */
292
+ get requests(): readonly MockRequestRecord[];
293
+ /** Empty the request log ({@link requests}). Routes are unaffected. */
294
+ clearRequests(): void;
295
+ /**
296
+ * Base URL to point an `OllamaClient` at. Only meaningful after
297
+ * {@link start} has resolved.
298
+ */
299
+ get baseUrl(): string;
300
+ /**
301
+ * Start listening. Resolves once the server is ready to accept
302
+ * connections.
303
+ */
304
+ start(): Promise<void>;
305
+ /**
306
+ * Stop listening and close all active connections. Resolves once the
307
+ * server is fully closed (no lingering sockets). Idempotent — calling
308
+ * `stop()` on an already-stopped server resolves cleanly rather than
309
+ * throwing `ERR_SERVER_NOT_RUNNING`.
310
+ */
311
+ stop(): Promise<void>;
312
+ /**
313
+ * Built-in default handler for a path, or `undefined` when defaults are
314
+ * disabled or the path isn't one of the official liveness surfaces.
315
+ * Registered routes are checked first and always win.
316
+ */
317
+ private defaultRouteFor;
318
+ private handleRequest;
319
+ }
320
+ /**
321
+ * Create an in-memory Ollama mock server — the recommended entry point for
322
+ * consumer test suites. Ephemeral port, built-in liveness + version
323
+ * defaults on, empty request log. Call `await server.start()` before
324
+ * pointing an `OllamaClient` at `server.baseUrl`, and `await server.stop()`
325
+ * in teardown.
326
+ *
327
+ * ```ts
328
+ * const server = createMockOllamaServer();
329
+ * await server.start();
330
+ * // defaults already answer: HEAD / (ping) and GET /api/version
331
+ * server.register('/api/chat', { chunks: mockTextChunks('Hi!') });
332
+ * ```
333
+ */
334
+ declare function createMockOllamaServer(options?: MockServerOptions): OllamaMockServer;
335
+
336
+ export { type MockEndpointOptions, type MockRequestRecord, type MockServerOptions, type MockTextChunksOptions, type MockToolCallChunksOptions, type MockToolCallSpec, OllamaMockServer, createMockOllamaServer, mockChatChunk, mockTextChunks, mockToolCallChunks };