@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.
- package/CHANGELOG.md +24 -0
- package/README.md +110 -4
- package/dist/{anthropic-B9AFyzXI.d.ts → anthropic-DrDUfbfQ.d.ts} +1 -1
- package/dist/{anthropic-CYy_ECam.d.cts → anthropic-UeaVjLeP.d.cts} +1 -1
- package/dist/{compose-6UOPEknN.d.ts → compose-CeFbHJeO.d.ts} +1 -1
- package/dist/{compose-DhvoUrWa.d.cts → compose-DFkmtUOP.d.cts} +1 -1
- package/dist/generated-api.d.cts +5 -5
- package/dist/generated-api.d.ts +5 -5
- package/dist/generated-runtime.d.cts +2 -2
- package/dist/generated-runtime.d.ts +2 -2
- package/dist/index.cjs +34 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -11
- package/dist/index.d.ts +40 -11
- package/dist/index.js +34 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp-generated.d.cts +2 -2
- package/dist/mcp-generated.d.ts +2 -2
- package/dist/{runtime-CIQYCRL3.d.ts → runtime-BIjr5ME4.d.ts} +1 -1
- package/dist/{runtime-DmI1cOEq.d.cts → runtime-C4RRHqOs.d.cts} +1 -1
- package/dist/skills.d.cts +3 -3
- package/dist/skills.d.ts +3 -3
- package/dist/testing.cjs +281 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +336 -0
- package/dist/testing.d.ts +336 -0
- package/dist/testing.js +275 -0
- package/dist/testing.js.map +1 -0
- package/dist/{types-B57ZZfp3.d.cts → types-DbPnfVh-.d.cts} +20 -3
- package/dist/{types-B57ZZfp3.d.ts → types-DbPnfVh-.d.ts} +20 -3
- package/package.json +14 -1
package/dist/mcp-generated.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as McpToolDescriptor } from './types-CI-gM2u5.cjs';
|
|
2
|
-
import { O as OllamaRuntime } from './runtime-
|
|
2
|
+
import { O as OllamaRuntime } from './runtime-C4RRHqOs.cjs';
|
|
3
3
|
import 'zod';
|
|
4
|
-
import './types-
|
|
4
|
+
import './types-DbPnfVh-.cjs';
|
|
5
5
|
|
|
6
6
|
interface GeneratedToolEntry extends McpToolDescriptor {
|
|
7
7
|
readonly _operationId: string;
|
package/dist/mcp-generated.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as McpToolDescriptor } from './types-CI-gM2u5.js';
|
|
2
|
-
import { O as OllamaRuntime } from './runtime-
|
|
2
|
+
import { O as OllamaRuntime } from './runtime-BIjr5ME4.js';
|
|
3
3
|
import 'zod';
|
|
4
|
-
import './types-
|
|
4
|
+
import './types-DbPnfVh-.js';
|
|
5
5
|
|
|
6
6
|
interface GeneratedToolEntry extends McpToolDescriptor {
|
|
7
7
|
readonly _operationId: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as Message, m as ChatResponse, i as ToolCall, b as ProgressResponse, o as GenerateResponse } from './types-
|
|
1
|
+
import { n as Message, m as ChatResponse, i as ToolCall, b as ProgressResponse, o as GenerateResponse } from './types-DbPnfVh-.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as Message, m as ChatResponse, i as ToolCall, b as ProgressResponse, o as GenerateResponse } from './types-
|
|
1
|
+
import { n as Message, m as ChatResponse, i as ToolCall, b as ProgressResponse, o as GenerateResponse } from './types-DbPnfVh-.cjs';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
/**
|
package/dist/skills.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { g as SkillSummary, S as Skill } from './compose-
|
|
2
|
-
export { d as AppliedSkillResult, e as ApplySkillOptions, P as ParsedFrontmatter, f as SkillFrontmatter, l as applySkill, p as parseFrontmatter } from './compose-
|
|
3
|
-
import './types-
|
|
1
|
+
import { g as SkillSummary, S as Skill } from './compose-DFkmtUOP.cjs';
|
|
2
|
+
export { d as AppliedSkillResult, e as ApplySkillOptions, P as ParsedFrontmatter, f as SkillFrontmatter, l as applySkill, p as parseFrontmatter } from './compose-DFkmtUOP.cjs';
|
|
3
|
+
import './types-DbPnfVh-.cjs';
|
|
4
4
|
import 'zod';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/skills.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { g as SkillSummary, S as Skill } from './compose-
|
|
2
|
-
export { d as AppliedSkillResult, e as ApplySkillOptions, P as ParsedFrontmatter, f as SkillFrontmatter, l as applySkill, p as parseFrontmatter } from './compose-
|
|
3
|
-
import './types-
|
|
1
|
+
import { g as SkillSummary, S as Skill } from './compose-CeFbHJeO.js';
|
|
2
|
+
export { d as AppliedSkillResult, e as ApplySkillOptions, P as ParsedFrontmatter, f as SkillFrontmatter, l as applySkill, p as parseFrontmatter } from './compose-CeFbHJeO.js';
|
|
3
|
+
import './types-DbPnfVh-.js';
|
|
4
4
|
import 'zod';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var http = require('http');
|
|
4
|
+
|
|
5
|
+
// src/testing/mock-server.ts
|
|
6
|
+
function nowIso() {
|
|
7
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
8
|
+
}
|
|
9
|
+
function mockChatChunk(frame) {
|
|
10
|
+
const done = frame.done ?? false;
|
|
11
|
+
const message = {
|
|
12
|
+
role: "assistant",
|
|
13
|
+
content: frame.content ?? ""
|
|
14
|
+
};
|
|
15
|
+
if (frame.toolCalls !== void 0 && frame.toolCalls.length > 0) {
|
|
16
|
+
message["tool_calls"] = frame.toolCalls.map((call, i) => ({
|
|
17
|
+
id: call.id ?? `call-${i}`,
|
|
18
|
+
function: {
|
|
19
|
+
name: call.name,
|
|
20
|
+
arguments: typeof call.arguments === "string" ? call.arguments : JSON.stringify(call.arguments)
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
const payload = {
|
|
25
|
+
model: frame.model ?? "mock-model",
|
|
26
|
+
created_at: nowIso(),
|
|
27
|
+
message,
|
|
28
|
+
done
|
|
29
|
+
};
|
|
30
|
+
if (done) {
|
|
31
|
+
payload["total_duration"] = frame.totalDurationNs ?? 5e7;
|
|
32
|
+
if (frame.loadDurationNs !== void 0) payload["load_duration"] = frame.loadDurationNs;
|
|
33
|
+
if (frame.promptEvalCount !== void 0) payload["prompt_eval_count"] = frame.promptEvalCount;
|
|
34
|
+
if (frame.promptEvalDurationNs !== void 0) {
|
|
35
|
+
payload["prompt_eval_duration"] = frame.promptEvalDurationNs;
|
|
36
|
+
}
|
|
37
|
+
if (frame.promptEvalCachedCount !== void 0) {
|
|
38
|
+
payload["prompt_eval_cached_count"] = frame.promptEvalCachedCount;
|
|
39
|
+
}
|
|
40
|
+
payload["eval_count"] = frame.evalCount ?? 2;
|
|
41
|
+
payload["eval_duration"] = frame.evalDurationNs ?? 2e7;
|
|
42
|
+
}
|
|
43
|
+
return JSON.stringify(payload);
|
|
44
|
+
}
|
|
45
|
+
function mockTextChunks(text, opts = {}) {
|
|
46
|
+
const model = opts.model ?? "mock-model";
|
|
47
|
+
const size = opts.chunkSize && opts.chunkSize > 0 ? opts.chunkSize : text.length || 1;
|
|
48
|
+
const frames = [];
|
|
49
|
+
if (text.length > 0) {
|
|
50
|
+
for (let i = 0; i < text.length; i += size) {
|
|
51
|
+
frames.push(mockChatChunk({ content: text.slice(i, i + size), model }));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
frames.push(
|
|
55
|
+
mockChatChunk({
|
|
56
|
+
model,
|
|
57
|
+
done: true,
|
|
58
|
+
evalCount: Math.max(1, Math.ceil(text.length / Math.max(1, size))),
|
|
59
|
+
evalDurationNs: 2e7,
|
|
60
|
+
totalDurationNs: 5e7
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
return frames;
|
|
64
|
+
}
|
|
65
|
+
function mockToolCallChunks(toolCalls, opts = {}) {
|
|
66
|
+
return [
|
|
67
|
+
mockChatChunk({ model: opts.model, content: opts.content, toolCalls }),
|
|
68
|
+
mockChatChunk({ model: opts.model, done: true, evalCount: 1, evalDurationNs: 1e7 })
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
var OllamaMockServer = class {
|
|
72
|
+
server;
|
|
73
|
+
routes = /* @__PURE__ */ new Map();
|
|
74
|
+
requestLog = [];
|
|
75
|
+
defaultsEnabled;
|
|
76
|
+
defaultVersion;
|
|
77
|
+
port;
|
|
78
|
+
/**
|
|
79
|
+
* @param portOrOptions Either a TCP port (pass `0` for an ephemeral port —
|
|
80
|
+
* recommended for parallel test runs) or a {@link MockServerOptions}
|
|
81
|
+
* object. The actual port is exposed via {@link baseUrl} after
|
|
82
|
+
* {@link start}.
|
|
83
|
+
*/
|
|
84
|
+
constructor(portOrOptions = 0) {
|
|
85
|
+
const options = typeof portOrOptions === "number" ? { port: portOrOptions } : portOrOptions;
|
|
86
|
+
this.port = options.port ?? 0;
|
|
87
|
+
this.defaultsEnabled = options.defaultRoutes ?? true;
|
|
88
|
+
this.defaultVersion = options.version ?? "0.14.0";
|
|
89
|
+
this.server = http.createServer((req, res) => {
|
|
90
|
+
void this.handleRequest(req, res);
|
|
91
|
+
});
|
|
92
|
+
this.server.on("connection", (socket) => {
|
|
93
|
+
socket.setNoDelay(true);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Register a route handler. Replaces any existing handler for the same
|
|
98
|
+
* path (including a built-in default). The path is matched against
|
|
99
|
+
* `req.url` exactly (query string stripped, no template matching — for
|
|
100
|
+
* `/api/blobs/{digest}`, register each specific digest value).
|
|
101
|
+
*/
|
|
102
|
+
register(path, options) {
|
|
103
|
+
this.routes.set(path, options);
|
|
104
|
+
}
|
|
105
|
+
/** Clear all registered routes (built-in defaults remain). Useful in `beforeEach` hooks. */
|
|
106
|
+
clear() {
|
|
107
|
+
this.routes.clear();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Every request this server has served, in arrival order — method, path,
|
|
111
|
+
* parsed body, timestamp — including 404s and default-route hits. Assert
|
|
112
|
+
* on what the client actually put on the wire; clear with
|
|
113
|
+
* {@link clearRequests} (e.g. in `beforeEach`).
|
|
114
|
+
*/
|
|
115
|
+
get requests() {
|
|
116
|
+
return this.requestLog;
|
|
117
|
+
}
|
|
118
|
+
/** Empty the request log ({@link requests}). Routes are unaffected. */
|
|
119
|
+
clearRequests() {
|
|
120
|
+
this.requestLog.length = 0;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Base URL to point an `OllamaClient` at. Only meaningful after
|
|
124
|
+
* {@link start} has resolved.
|
|
125
|
+
*/
|
|
126
|
+
get baseUrl() {
|
|
127
|
+
const address = this.server.address();
|
|
128
|
+
const port = address?.port ?? this.port;
|
|
129
|
+
return `http://127.0.0.1:${port}`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Start listening. Resolves once the server is ready to accept
|
|
133
|
+
* connections.
|
|
134
|
+
*/
|
|
135
|
+
start() {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
this.server.once("error", reject);
|
|
138
|
+
this.server.listen(this.port, "127.0.0.1", () => {
|
|
139
|
+
this.server.removeListener("error", reject);
|
|
140
|
+
const address = this.server.address();
|
|
141
|
+
if (address !== null) {
|
|
142
|
+
this.port = address.port;
|
|
143
|
+
}
|
|
144
|
+
resolve();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Stop listening and close all active connections. Resolves once the
|
|
150
|
+
* server is fully closed (no lingering sockets). Idempotent — calling
|
|
151
|
+
* `stop()` on an already-stopped server resolves cleanly rather than
|
|
152
|
+
* throwing `ERR_SERVER_NOT_RUNNING`.
|
|
153
|
+
*/
|
|
154
|
+
stop() {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
if (!this.server.listening) {
|
|
157
|
+
resolve();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.server.close((err) => {
|
|
161
|
+
if (err && err.code !== "ERR_SERVER_NOT_RUNNING") {
|
|
162
|
+
reject(err);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
resolve();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Built-in default handler for a path, or `undefined` when defaults are
|
|
171
|
+
* disabled or the path isn't one of the official liveness surfaces.
|
|
172
|
+
* Registered routes are checked first and always win.
|
|
173
|
+
*/
|
|
174
|
+
defaultRouteFor(method, path) {
|
|
175
|
+
if (!this.defaultsEnabled) {
|
|
176
|
+
return void 0;
|
|
177
|
+
}
|
|
178
|
+
if (path === "/" && (method === "HEAD" || method === "GET")) {
|
|
179
|
+
return {
|
|
180
|
+
status: 200,
|
|
181
|
+
headers: { "content-type": "text/plain" },
|
|
182
|
+
// GET / answers the plain-text body byte-for-byte — no trailing
|
|
183
|
+
// newline, hence rawBody rather than the NDJSON chunk emitter.
|
|
184
|
+
// HEAD / carries no body at all (Node suppresses HEAD writes).
|
|
185
|
+
...method === "GET" ? { rawBody: "Ollama is running" } : {}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (path === "/api/version" && method === "GET") {
|
|
189
|
+
return {
|
|
190
|
+
status: 200,
|
|
191
|
+
headers: { "content-type": "application/json" },
|
|
192
|
+
chunks: [JSON.stringify({ version: this.defaultVersion })]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
async handleRequest(req, res) {
|
|
198
|
+
const url = req.url ?? "/";
|
|
199
|
+
const path = url.split("?")[0] ?? "/";
|
|
200
|
+
const method = req.method ?? "GET";
|
|
201
|
+
let parsedBody;
|
|
202
|
+
if (method === "POST" || method === "PUT") {
|
|
203
|
+
const bodyChunks = [];
|
|
204
|
+
for await (const chunk of req) {
|
|
205
|
+
bodyChunks.push(chunk);
|
|
206
|
+
}
|
|
207
|
+
const bodyText = Buffer.concat(bodyChunks).toString("utf-8");
|
|
208
|
+
if (bodyText) {
|
|
209
|
+
try {
|
|
210
|
+
parsedBody = JSON.parse(bodyText);
|
|
211
|
+
} catch {
|
|
212
|
+
parsedBody = bodyText;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this.requestLog.push({
|
|
217
|
+
method,
|
|
218
|
+
path,
|
|
219
|
+
...parsedBody !== void 0 ? { body: parsedBody } : {},
|
|
220
|
+
timestamp: nowIso()
|
|
221
|
+
});
|
|
222
|
+
const route = this.routes.get(path) ?? this.defaultRouteFor(method, path);
|
|
223
|
+
if (!route) {
|
|
224
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
225
|
+
res.end(JSON.stringify({ error: `Route not mocked: ${path}` }));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
route.onRequest?.(parsedBody);
|
|
229
|
+
const status = route.status ?? 200;
|
|
230
|
+
const chunks = route.chunks ?? [];
|
|
231
|
+
const chunkDelayMs = route.chunkDelayMs ?? 0;
|
|
232
|
+
const headers = route.headers ?? { "content-type": "application/x-ndjson" };
|
|
233
|
+
res.writeHead(status, headers);
|
|
234
|
+
if (route.rawBody !== void 0) {
|
|
235
|
+
if (chunkDelayMs > 0) {
|
|
236
|
+
await new Promise((r) => setTimeout(r, chunkDelayMs));
|
|
237
|
+
}
|
|
238
|
+
res.write(route.rawBody);
|
|
239
|
+
res.end();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (route.dropConnectionMidStream) {
|
|
243
|
+
if (chunks.length > 0) {
|
|
244
|
+
if (chunkDelayMs > 0) {
|
|
245
|
+
await new Promise((r) => setTimeout(r, chunkDelayMs));
|
|
246
|
+
}
|
|
247
|
+
res.write(chunks[0] + "\n");
|
|
248
|
+
}
|
|
249
|
+
res.socket?.destroy();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const errorAt = route.injectErrorFrame ? route.injectErrorFrame.after ?? Math.max(0, chunks.length - 1) : Number.POSITIVE_INFINITY;
|
|
253
|
+
const errorMessage = route.injectErrorFrame?.message;
|
|
254
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
255
|
+
if (i === errorAt && errorMessage !== void 0) {
|
|
256
|
+
res.write(JSON.stringify({ error: errorMessage }) + "\n");
|
|
257
|
+
res.end();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (chunkDelayMs > 0) {
|
|
261
|
+
await new Promise((r) => setTimeout(r, chunkDelayMs));
|
|
262
|
+
}
|
|
263
|
+
res.write(chunks[i] + "\n");
|
|
264
|
+
}
|
|
265
|
+
if (errorMessage !== void 0 && chunks.length <= errorAt) {
|
|
266
|
+
res.write(JSON.stringify({ error: errorMessage }) + "\n");
|
|
267
|
+
}
|
|
268
|
+
res.end();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
function createMockOllamaServer(options = {}) {
|
|
272
|
+
return new OllamaMockServer(options);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
exports.OllamaMockServer = OllamaMockServer;
|
|
276
|
+
exports.createMockOllamaServer = createMockOllamaServer;
|
|
277
|
+
exports.mockChatChunk = mockChatChunk;
|
|
278
|
+
exports.mockTextChunks = mockTextChunks;
|
|
279
|
+
exports.mockToolCallChunks = mockToolCallChunks;
|
|
280
|
+
//# sourceMappingURL=testing.cjs.map
|
|
281
|
+
//# sourceMappingURL=testing.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/testing/mock-server.ts"],"names":["createServer"],"mappings":"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","file":"testing.cjs","sourcesContent":["/**\n * In-memory Ollama mock server for deterministic, zero-network CI testing.\n *\n * This is the SDK's own mock engine, promoted from the internal test fixture\n * (`test/mocks/ollama-mock-server.ts`) to a public entry point\n * (`@nemesis-oss/ollama-sdk/testing`) so consumers don't have to hand-roll\n * ad-hoc HTTP stubs to test their Ollama integrations — see ADR 0029.\n * The internal fixture re-exports this module, so the SDK's own test suite\n * exercises the exact same code consumers get.\n *\n * The VCR cassette system (`test/vcr.ts` in the repo) replays recorded HTTP\n * interactions, but cassettes are static. This mock server covers what\n * cassettes can't:\n *\n * - programmable inter-chunk delays (backpressure handling)\n * - mid-stream **in-band error injection** — an `{\"error\": \"...\"}` NDJSON\n * frame emitted mid-response with HTTP 200 still, exactly how a real\n * Ollama daemon reports GPU OOM / driver crashes mid-generation\n * - connection drops mid-stream (`dropConnectionMidStream`)\n * - 503/502/429 error status codes with custom bodies\n * - arbitrary NDJSON chunk payloads (tool calls, thinking, partial frames)\n *\n * On top of the raw engine, the public surface adds the conveniences a\n * consumer test suite wants on day one:\n *\n * - {@link createMockOllamaServer} factory (the recommended entry point)\n * - built-in **default routes** — the official liveness probe (`HEAD /`\n * and `GET /` → \"Ollama is running\") and `GET /api/version` — so\n * `client.ping()` and friends work before any route is registered.\n * Explicit `register()` calls always win.\n * - **request recording** (`server.requests`, `clearRequests()`) for\n * wire-level assertions without per-route callbacks\n * - NDJSON **chunk builders** ({@link mockChatChunk}, {@link mockTextChunks},\n * {@link mockToolCallChunks}) that produce correctly-shaped `/api/chat`\n * frames — including parallel tool calls in one frame, the documented\n * native behavior\n *\n * ## Usage\n *\n * ```ts\n * import { createMockOllamaServer, mockToolCallChunks } from '@nemesis-oss/ollama-sdk/testing';\n * import { OllamaClient } from '@nemesis-oss/ollama-sdk';\n *\n * const server = createMockOllamaServer(); // ephemeral port, default routes on\n * await server.start();\n *\n * server.register('/api/chat', {\n * chunks: mockToolCallChunks([\n * { name: 'get_weather', arguments: { city: 'Tokyo' } },\n * { name: 'get_time', arguments: { tz: 'Asia/Tokyo' } },\n * ]),\n * });\n *\n * const client = new OllamaClient({ baseUrl: server.baseUrl });\n * // ... exercise the client; assert on server.requests ...\n *\n * await server.stop();\n * ```\n *\n * ## Environment\n *\n * Node.js only — the module builds on `node:http`, which is why it lives in\n * a dedicated subpath export: importing it from the browser/Edge-safe main\n * entry would drag a Node built-in into every bundle. Never re-export this\n * module from `src/index.ts`; the Edge Runtime verification\n * (`npm run verify:edge-runtime`) exists to catch exactly that.\n *\n * Designed for Vitest/Jest: `start()` resolves once the server listens and\n * `stop()` is idempotent, so `beforeEach`/`afterEach` hooks manage the\n * lifecycle without tracking whether `start()` ran.\n */\n\nimport { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\n\n/** One recorded incoming request, for wire-level test assertions. */\nexport interface MockRequestRecord {\n /** HTTP method (`GET`, `POST`, `HEAD`, ...). */\n readonly method: string;\n /** Request path with the query string stripped. */\n readonly path: string;\n /** Parsed request body (JSON-parsed when possible, raw string otherwise); `undefined` for body-less methods. */\n readonly body?: unknown;\n /** ISO-8601 timestamp of when the request arrived. */\n readonly timestamp: string;\n}\n\n/**\n * A single mock route handler. The server matches on the request path\n * (query string stripped — Ollama's API doesn't use query params for the\n * routes we care about).\n *\n * All fields optional except `chunks` (which defaults to `[]` — useful for\n * testing the \"stream closed without any data\" case).\n */\nexport interface MockEndpointOptions {\n /** HTTP status code. Defaults to 200. */\n readonly status?: number | undefined;\n /**\n * NDJSON chunks to emit, in order. Each chunk is emitted as a separate\n * `res.write()` call followed by `\\n`. Pass `[]` to emit no body (useful\n * for testing the \"stream closed without data\" case).\n */\n readonly chunks?: readonly string[] | undefined;\n /**\n * Delay (ms) before emitting each chunk. Defaults to 0 (no delay). Set to\n * a small value (5-50ms) to test backpressure handling; set to a large\n * value (10_000ms) to test timeout/cancellation.\n */\n readonly chunkDelayMs?: number | undefined;\n /**\n * Custom response headers. Defaults to\n * `{ 'content-type': 'application/x-ndjson' }` for streaming endpoints.\n * Override for non-JSON responses (e.g. blob uploads).\n */\n readonly headers?: Record<string, string> | undefined;\n /**\n * When `true`, the server closes the connection mid-stream after emitting\n * the first chunk. Use this to test connection-drop recovery. Defaults to\n * `false`.\n */\n readonly dropConnectionMidStream?: boolean | undefined;\n /**\n * Emit this exact string as the whole response body — no NDJSON newline\n * framing. Overrides `chunks` (use it for non-JSON responses such as the\n * plain-text liveness body or raw blob bytes).\n */\n readonly rawBody?: string | undefined;\n /**\n * Inject an in-band error frame — `{\"error\": \"<message>\"}` — into the\n * NDJSON response while the HTTP status stays 200. This is how a real\n * Ollama daemon reports mid-generation failures (GPU OOM, driver crash,\n * context overflow): the status code never changes, the parser has to\n * catch the frame. The SDK's stream readers raise `OllamaStreamError` on\n * it.\n *\n * `after` is the number of chunks emitted before the error frame. The\n * default is `chunks.length - 1` — every content chunk, then the error\n * frame, with the final `done` frame never emitted, exactly what a\n * mid-generation crash looks like on the wire (an error frame *after* the\n * done frame would be unread: the stream has already completed cleanly).\n * `after: 0` errors before any content; an explicit `after >=\n * chunks.length` emits every chunk and then the error. Chunks at or after\n * the injection point are never emitted: the server \"crashed\".\n */\n readonly injectErrorFrame?:\n | {\n readonly after?: number | undefined;\n readonly message: string;\n }\n | undefined;\n /**\n * Optional callback fired when a request arrives at this route. Receives\n * the parsed request body (or `undefined` if the body wasn't valid JSON).\n * Useful for assertions like \"the client sent the right `model` field\".\n * For global capture across routes see {@link OllamaMockServer.requests}.\n */\n readonly onRequest?: ((body: unknown) => void) | undefined;\n}\n\n/** Options for {@link createMockOllamaServer} and the `OllamaMockServer` constructor. */\nexport interface MockServerOptions {\n /** TCP port to listen on. `0` (the default) picks an ephemeral port. */\n readonly port?: number | undefined;\n /**\n * Serve the built-in default routes when no route is explicitly registered\n * for a path: `HEAD /` and `GET /` → `200` (the official liveness probe —\n * `GET /` answers the plain-text body \"Ollama is running\") and\n * `GET /api/version` → `200 {\"version\": \"<version>\"}`. Defaults to `true`.\n * Explicit `register()` calls always take precedence.\n */\n readonly defaultRoutes?: boolean | undefined;\n /** Version reported by the default `GET /api/version` route. Defaults to `'0.14.0'`. */\n readonly version?: string | undefined;\n}\n\n/** Shape accepted by {@link mockToolCallChunks} for each tool call. */\nexport interface MockToolCallSpec {\n /** Tool-call id; auto-generated (`call-0`, `call-1`, …) when omitted. */\n readonly id?: string | undefined;\n /** Tool name, as the model would emit it. */\n readonly name: string;\n /** Tool arguments — an object (stringified by the builder) or a pre-encoded string. */\n readonly arguments: Record<string, unknown> | string;\n}\n\n/** Options for {@link mockTextChunks}. */\nexport interface MockTextChunksOptions {\n /** Split the text into content frames of at most this many characters. Default: one frame with the whole text. */\n readonly chunkSize?: number | undefined;\n /** Model name stamped on each frame. Defaults to `'mock-model'`. */\n readonly model?: string | undefined;\n}\n\n/** Options for {@link mockToolCallChunks}. */\nexport interface MockToolCallChunksOptions {\n /** Model name stamped on each frame. Defaults to `'mock-model'`. */\n readonly model?: string | undefined;\n /** Assistant text to carry on the tool-call frame (some models emit a preamble). Defaults to `''`. */\n readonly content?: string | undefined;\n}\n\nfunction nowIso(): string {\n return new Date().toISOString();\n}\n\n/**\n * One `/api/chat` NDJSON frame as a ready-to-emit string. Sensible defaults:\n * `model: 'mock-model'`, a fresh `created_at`, `done: false`.\n *\n * ```ts\n * server.register('/api/chat', {\n * chunks: [\n * mockChatChunk({ content: 'Hel' }),\n * mockChatChunk({ content: 'lo' }),\n * mockChatChunk({ done: true, evalCount: 2, evalDurationNs: 20_000_000 }),\n * ],\n * });\n * ```\n */\nexport function mockChatChunk(frame: {\n readonly content?: string | undefined;\n readonly toolCalls?: readonly MockToolCallSpec[] | undefined;\n readonly done?: boolean | undefined;\n readonly model?: string | undefined;\n readonly evalCount?: number | undefined;\n readonly evalDurationNs?: number | undefined;\n readonly promptEvalCount?: number | undefined;\n readonly promptEvalDurationNs?: number | undefined;\n readonly promptEvalCachedCount?: number | undefined;\n readonly totalDurationNs?: number | undefined;\n readonly loadDurationNs?: number | undefined;\n}): string {\n const done = frame.done ?? false;\n const message: Record<string, unknown> = {\n role: 'assistant',\n content: frame.content ?? '',\n };\n if (frame.toolCalls !== undefined && frame.toolCalls.length > 0) {\n message['tool_calls'] = frame.toolCalls.map((call, i) => ({\n id: call.id ?? `call-${i}`,\n function: {\n name: call.name,\n arguments:\n typeof call.arguments === 'string' ? call.arguments : JSON.stringify(call.arguments),\n },\n }));\n }\n const payload: Record<string, unknown> = {\n model: frame.model ?? 'mock-model',\n created_at: nowIso(),\n message,\n done,\n };\n if (done) {\n // Counters on the final frame — shaped exactly like the official\n // /api/chat streaming examples so telemetry code paths see real fields.\n payload['total_duration'] = frame.totalDurationNs ?? 50_000_000;\n if (frame.loadDurationNs !== undefined) payload['load_duration'] = frame.loadDurationNs;\n if (frame.promptEvalCount !== undefined) payload['prompt_eval_count'] = frame.promptEvalCount;\n if (frame.promptEvalDurationNs !== undefined) {\n payload['prompt_eval_duration'] = frame.promptEvalDurationNs;\n }\n if (frame.promptEvalCachedCount !== undefined) {\n payload['prompt_eval_cached_count'] = frame.promptEvalCachedCount;\n }\n payload['eval_count'] = frame.evalCount ?? 2;\n payload['eval_duration'] = frame.evalDurationNs ?? 20_000_000;\n }\n return JSON.stringify(payload);\n}\n\n/**\n * Content frames + a final `done` frame for a plain-text `/api/chat` reply.\n * With `chunkSize`, the text is split into that many characters per frame —\n * the shape streaming code needs to exercise incremental assembly.\n *\n * ```ts\n * server.register('/api/chat', {\n * chunks: mockTextChunks('Hello, world!', { chunkSize: 5 }),\n * });\n * ```\n */\nexport function mockTextChunks(text: string, opts: MockTextChunksOptions = {}): string[] {\n const model = opts.model ?? 'mock-model';\n const size = opts.chunkSize && opts.chunkSize > 0 ? opts.chunkSize : text.length || 1;\n const frames: string[] = [];\n if (text.length > 0) {\n for (let i = 0; i < text.length; i += size) {\n frames.push(mockChatChunk({ content: text.slice(i, i + size), model }));\n }\n }\n frames.push(\n mockChatChunk({\n model,\n done: true,\n evalCount: Math.max(1, Math.ceil(text.length / Math.max(1, size))),\n evalDurationNs: 20_000_000,\n totalDurationNs: 50_000_000,\n }),\n );\n return frames;\n}\n\n/**\n * Tool-call frames for `/api/chat` — the documented native shape: tool calls\n * arrive **complete in one frame** (parallel calls = one array with several\n * entries), followed by a final `done` frame. Pair with an `Agent` or tool\n * loop for deterministic tool-execution tests.\n *\n * ```ts\n * // Parallel tool calls, one round trip:\n * server.register('/api/chat', {\n * chunks: mockToolCallChunks([\n * { name: 'get_weather', arguments: { city: 'Tokyo' } },\n * { name: 'get_time', arguments: { tz: 'Asia/Tokyo' } },\n * ]),\n * });\n * ```\n */\nexport function mockToolCallChunks(\n toolCalls: readonly MockToolCallSpec[],\n opts: MockToolCallChunksOptions = {},\n): string[] {\n return [\n mockChatChunk({ model: opts.model, content: opts.content, toolCalls }),\n mockChatChunk({ model: opts.model, done: true, evalCount: 1, evalDurationNs: 10_000_000 }),\n ];\n}\n\n/**\n * Lightweight in-memory Ollama mock server.\n *\n * Spawns a real `node:http` server on a free port (or a fixed port if you\n * pass one). Tests register route handlers via `register()`, then point an\n * `OllamaClient` at `server.baseUrl`. The server emits the registered chunks\n * with the configured delay, status, and connection behavior; when no route\n * is registered the built-in liveness + version defaults answer (see\n * {@link MockServerOptions.defaultRoutes}).\n *\n * Prefer the {@link createMockOllamaServer} factory; the class is exported\n * for typed references and back-compatibility with the SDK's own suite.\n *\n * The server is **single-process** — it doesn't fork or use workers. It's\n * intended for unit/integration tests, not load testing.\n */\nexport class OllamaMockServer {\n private readonly server: Server;\n private readonly routes = new Map<string, MockEndpointOptions>();\n private readonly requestLog: MockRequestRecord[] = [];\n private readonly defaultsEnabled: boolean;\n private readonly defaultVersion: string;\n private port: number;\n\n /**\n * @param portOrOptions Either a TCP port (pass `0` for an ephemeral port —\n * recommended for parallel test runs) or a {@link MockServerOptions}\n * object. The actual port is exposed via {@link baseUrl} after\n * {@link start}.\n */\n constructor(portOrOptions: number | MockServerOptions = 0) {\n const options: MockServerOptions =\n typeof portOrOptions === 'number' ? { port: portOrOptions } : portOrOptions;\n this.port = options.port ?? 0;\n this.defaultsEnabled = options.defaultRoutes ?? true;\n this.defaultVersion = options.version ?? '0.14.0';\n this.server = createServer((req, res) => {\n void this.handleRequest(req, res);\n });\n // Disable Nagle's algorithm so chunks are emitted immediately rather\n // than being coalesced — makes delay/fracture behavior deterministic.\n this.server.on('connection', (socket) => {\n socket.setNoDelay(true);\n });\n }\n\n /**\n * Register a route handler. Replaces any existing handler for the same\n * path (including a built-in default). The path is matched against\n * `req.url` exactly (query string stripped, no template matching — for\n * `/api/blobs/{digest}`, register each specific digest value).\n */\n register(path: string, options: MockEndpointOptions): void {\n this.routes.set(path, options);\n }\n\n /** Clear all registered routes (built-in defaults remain). Useful in `beforeEach` hooks. */\n clear(): void {\n this.routes.clear();\n }\n\n /**\n * Every request this server has served, in arrival order — method, path,\n * parsed body, timestamp — including 404s and default-route hits. Assert\n * on what the client actually put on the wire; clear with\n * {@link clearRequests} (e.g. in `beforeEach`).\n */\n get requests(): readonly MockRequestRecord[] {\n return this.requestLog;\n }\n\n /** Empty the request log ({@link requests}). Routes are unaffected. */\n clearRequests(): void {\n this.requestLog.length = 0;\n }\n\n /**\n * Base URL to point an `OllamaClient` at. Only meaningful after\n * {@link start} has resolved.\n */\n get baseUrl(): string {\n const address = this.server.address() as AddressInfo | null;\n const port = address?.port ?? this.port;\n return `http://127.0.0.1:${port}`;\n }\n\n /**\n * Start listening. Resolves once the server is ready to accept\n * connections.\n */\n start(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server.once('error', reject);\n this.server.listen(this.port, '127.0.0.1', () => {\n this.server.removeListener('error', reject);\n const address = this.server.address() as AddressInfo | null;\n if (address !== null) {\n this.port = address.port;\n }\n resolve();\n });\n });\n }\n\n /**\n * Stop listening and close all active connections. Resolves once the\n * server is fully closed (no lingering sockets). Idempotent — calling\n * `stop()` on an already-stopped server resolves cleanly rather than\n * throwing `ERR_SERVER_NOT_RUNNING`.\n */\n stop(): Promise<void> {\n return new Promise((resolve, reject) => {\n // `server.close()` errors with ERR_SERVER_NOT_RUNNING if the server\n // isn't currently listening. Treat that as a successful no-op so\n // callers can use stop() in afterEach hooks without tracking whether\n // start() was actually called.\n if (!this.server.listening) {\n resolve();\n return;\n }\n this.server.close((err) => {\n if (err && (err as NodeJS.ErrnoException).code !== 'ERR_SERVER_NOT_RUNNING') {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n\n /**\n * Built-in default handler for a path, or `undefined` when defaults are\n * disabled or the path isn't one of the official liveness surfaces.\n * Registered routes are checked first and always win.\n */\n private defaultRouteFor(method: string, path: string): MockEndpointOptions | undefined {\n if (!this.defaultsEnabled) {\n return undefined;\n }\n // The official liveness probe: HEAD / → 200 (no body — Node suppresses\n // writes for HEAD responses); GET / → the plain-text body\n // \"Ollama is running\" (docs: the root endpoint answers exactly this).\n if (path === '/' && (method === 'HEAD' || method === 'GET')) {\n return {\n status: 200,\n headers: { 'content-type': 'text/plain' },\n // GET / answers the plain-text body byte-for-byte — no trailing\n // newline, hence rawBody rather than the NDJSON chunk emitter.\n // HEAD / carries no body at all (Node suppresses HEAD writes).\n ...(method === 'GET' ? { rawBody: 'Ollama is running' } : {}),\n };\n }\n if (path === '/api/version' && method === 'GET') {\n return {\n status: 200,\n headers: { 'content-type': 'application/json' },\n chunks: [JSON.stringify({ version: this.defaultVersion })],\n };\n }\n return undefined;\n }\n\n private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = req.url ?? '/';\n // Strip query string for route matching — Ollama's API doesn't use\n // query params for the routes we care about, and stripping makes the\n // test setup simpler.\n const path = url.split('?')[0] ?? '/';\n const method = req.method ?? 'GET';\n\n // Capture the request body for recording + the route's onRequest.\n let parsedBody: unknown;\n if (method === 'POST' || method === 'PUT') {\n const bodyChunks: Buffer[] = [];\n for await (const chunk of req) {\n bodyChunks.push(chunk as Buffer);\n }\n const bodyText = Buffer.concat(bodyChunks).toString('utf-8');\n if (bodyText) {\n try {\n parsedBody = JSON.parse(bodyText);\n } catch {\n parsedBody = bodyText;\n }\n }\n }\n\n this.requestLog.push({\n method,\n path,\n ...(parsedBody !== undefined ? { body: parsedBody } : {}),\n timestamp: nowIso(),\n });\n\n const route = this.routes.get(path) ?? this.defaultRouteFor(method, path);\n\n if (!route) {\n res.writeHead(404, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ error: `Route not mocked: ${path}` }));\n return;\n }\n\n route.onRequest?.(parsedBody);\n\n const status = route.status ?? 200;\n const chunks = route.chunks ?? [];\n const chunkDelayMs = route.chunkDelayMs ?? 0;\n const headers = route.headers ?? { 'content-type': 'application/x-ndjson' };\n\n res.writeHead(status, headers);\n\n if (route.rawBody !== undefined) {\n if (chunkDelayMs > 0) {\n await new Promise((r) => setTimeout(r, chunkDelayMs));\n }\n res.write(route.rawBody);\n res.end();\n return;\n }\n\n if (route.dropConnectionMidStream) {\n // Emit the first chunk (if any), then destroy the socket without\n // ending the response — a network drop mid-stream.\n if (chunks.length > 0) {\n if (chunkDelayMs > 0) {\n await new Promise((r) => setTimeout(r, chunkDelayMs));\n }\n res.write(chunks[0] + '\\n');\n }\n res.socket?.destroy();\n return;\n }\n\n // In-band injection point, measured in already-emitted chunks. See\n // MockEndpointOptions.injectErrorFrame for the default rationale.\n const errorAt = route.injectErrorFrame\n ? (route.injectErrorFrame.after ?? Math.max(0, chunks.length - 1))\n : Number.POSITIVE_INFINITY;\n const errorMessage = route.injectErrorFrame?.message;\n\n for (let i = 0; i < chunks.length; i++) {\n if (i === errorAt && errorMessage !== undefined) {\n // The server \"crashed\": emit the error frame, never the remaining\n // chunks (no clean done frame — the stream just ends, HTTP 200).\n res.write(JSON.stringify({ error: errorMessage }) + '\\n');\n res.end();\n return;\n }\n if (chunkDelayMs > 0) {\n await new Promise((r) => setTimeout(r, chunkDelayMs));\n }\n res.write(chunks[i] + '\\n');\n }\n // Explicit `after` at or beyond the chunk count: everything, then error.\n if (errorMessage !== undefined && chunks.length <= errorAt) {\n res.write(JSON.stringify({ error: errorMessage }) + '\\n');\n }\n res.end();\n }\n}\n\n/**\n * Create an in-memory Ollama mock server — the recommended entry point for\n * consumer test suites. Ephemeral port, built-in liveness + version\n * defaults on, empty request log. Call `await server.start()` before\n * pointing an `OllamaClient` at `server.baseUrl`, and `await server.stop()`\n * in teardown.\n *\n * ```ts\n * const server = createMockOllamaServer();\n * await server.start();\n * // defaults already answer: HEAD / (ping) and GET /api/version\n * server.register('/api/chat', { chunks: mockTextChunks('Hi!') });\n * ```\n */\nexport function createMockOllamaServer(options: MockServerOptions = {}): OllamaMockServer {\n return new OllamaMockServer(options);\n}\n"]}
|