@fgv/ts-extras-ollama 5.1.0-34

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.
Files changed (65) hide show
  1. package/.rush/temp/b70e5f6b6ada97ea70c5583027ea2b58b27bef46.tar.log +54 -0
  2. package/.rush/temp/chunked-rush-logs/ts-extras-ollama.build.chunks.jsonl +9 -0
  3. package/.rush/temp/operation/build/all.log +9 -0
  4. package/.rush/temp/operation/build/log-chunks.jsonl +9 -0
  5. package/.rush/temp/operation/build/state.json +3 -0
  6. package/.rush/temp/shrinkwrap-deps.json +681 -0
  7. package/CHANGELOG.json +4 -0
  8. package/README.md +105 -0
  9. package/config/api-extractor.json +38 -0
  10. package/config/jest.config.json +13 -0
  11. package/config/rig.json +6 -0
  12. package/dist/index.js +388 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/test/unit/chatStructured.test.js +287 -0
  15. package/dist/test/unit/chatStructured.test.js.map +1 -0
  16. package/dist/test/unit/fixtures/wireFixtures.js +90 -0
  17. package/dist/test/unit/fixtures/wireFixtures.js.map +1 -0
  18. package/dist/test/unit/modelManagement.test.js +118 -0
  19. package/dist/test/unit/modelManagement.test.js.map +1 -0
  20. package/dist/test/unit/ollamaClient.test.js +38 -0
  21. package/dist/test/unit/ollamaClient.test.js.map +1 -0
  22. package/dist/test/unit/pullModel.test.js +202 -0
  23. package/dist/test/unit/pullModel.test.js.map +1 -0
  24. package/dist/ts-extras-ollama.d.ts +365 -0
  25. package/dist/tsdoc-metadata.json +11 -0
  26. package/eslint.config.js +15 -0
  27. package/etc/ts-extras-ollama.api.md +139 -0
  28. package/lib/index.d.ts +341 -0
  29. package/lib/index.d.ts.map +1 -0
  30. package/lib/index.js +397 -0
  31. package/lib/index.js.map +1 -0
  32. package/lib/test/unit/chatStructured.test.d.ts +2 -0
  33. package/lib/test/unit/chatStructured.test.d.ts.map +1 -0
  34. package/lib/test/unit/chatStructured.test.js +289 -0
  35. package/lib/test/unit/chatStructured.test.js.map +1 -0
  36. package/lib/test/unit/fixtures/wireFixtures.d.ts +19 -0
  37. package/lib/test/unit/fixtures/wireFixtures.d.ts.map +1 -0
  38. package/lib/test/unit/fixtures/wireFixtures.js +93 -0
  39. package/lib/test/unit/fixtures/wireFixtures.js.map +1 -0
  40. package/lib/test/unit/modelManagement.test.d.ts +2 -0
  41. package/lib/test/unit/modelManagement.test.d.ts.map +1 -0
  42. package/lib/test/unit/modelManagement.test.js +120 -0
  43. package/lib/test/unit/modelManagement.test.js.map +1 -0
  44. package/lib/test/unit/ollamaClient.test.d.ts +2 -0
  45. package/lib/test/unit/ollamaClient.test.d.ts.map +1 -0
  46. package/lib/test/unit/ollamaClient.test.js +40 -0
  47. package/lib/test/unit/ollamaClient.test.js.map +1 -0
  48. package/lib/test/unit/pullModel.test.d.ts +2 -0
  49. package/lib/test/unit/pullModel.test.d.ts.map +1 -0
  50. package/lib/test/unit/pullModel.test.js +204 -0
  51. package/lib/test/unit/pullModel.test.js.map +1 -0
  52. package/package.json +83 -0
  53. package/rush-logs/ts-extras-ollama.build.cache.log +3 -0
  54. package/rush-logs/ts-extras-ollama.build.log +9 -0
  55. package/src/index.ts +655 -0
  56. package/src/test/unit/chatStructured.test.ts +330 -0
  57. package/src/test/unit/fixtures/wireFixtures.ts +95 -0
  58. package/src/test/unit/modelManagement.test.ts +128 -0
  59. package/src/test/unit/ollamaClient.test.ts +44 -0
  60. package/src/test/unit/pullModel.test.ts +220 -0
  61. package/temp/build/lint/_eslint-5eVG3S6w.json +30 -0
  62. package/temp/build/typescript/ts_8nwakTlr.json +1 -0
  63. package/temp/ts-extras-ollama.api.json +2528 -0
  64. package/temp/ts-extras-ollama.api.md +139 -0
  65. package/tsconfig.json +8 -0
package/CHANGELOG.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "@fgv/ts-extras-ollama",
3
+ "entries": []
4
+ }
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @fgv/ts-extras-ollama
2
+
3
+ Result-integration boundary over the official [`ollama`](https://github.com/ollama/ollama-js) JS
4
+ library for Node-side consumers. Owns **exactly and only** the native-Ollama surface that the
5
+ OpenAI-compatible `/v1` endpoint cannot express.
6
+
7
+ **Status:** v0.1 surface complete (the `ollama-native` stream, phases O-1…O-4). Native `embed` is
8
+ held pending the cross-provider `ai-assist-embeddings` design.
9
+
10
+ ---
11
+
12
+ ## What this is
13
+
14
+ A thin facade that wraps the `ollama` client's native-API calls in `Result<T>` from
15
+ `@fgv/ts-utils`, mirroring the discipline established by
16
+ [`@fgv/ts-extras-webauthn`](../ts-extras-webauthn) and
17
+ [`@fgv/ts-extras-transformers`](../ts-extras-transformers): one-line `captureAsyncResult` /
18
+ `captureResult` wrappers around upstream primitives with **no opinionated orchestration** above the
19
+ boundary. Consumers compose the primitives themselves.
20
+
21
+ The text-completion / streaming / tool-use path is **not** here — `@fgv/ts-extras/ai-assist` owns it
22
+ via the `/v1` compat layer (point a provider descriptor's `endpoint` at `http://localhost:11434/v1`
23
+ and call `callProviderCompletion` / `callProviderCompletionStream` / `executeClientToolTurn`).
24
+
25
+ ## The native-only surface
26
+
27
+ | Function | Wraps | Status |
28
+ |---|---|---|
29
+ | `createOllamaClient(params?)` | `new Ollama(...)` | O-1 |
30
+ | `listModels(client)` | `client.list()` (`/api/tags`) | O-2 |
31
+ | `showModel(client, model)` | `client.show(...)` (`/api/show`) | O-2 |
32
+ | `listRunning(client)` | `client.ps()` (`/api/ps`) | O-2 |
33
+ | `deleteModel(client, model)` | `client.delete(...)` (`/api/delete`) | O-2 |
34
+ | `pullModel(client, params)` | `client.pull({ stream: true })` (`/api/pull`) | O-3 |
35
+ | `chatStructured(client, params)` | `client.chat({ format })` (`/api/chat`) | O-4 |
36
+
37
+ ## Quick start
38
+
39
+ ```typescript
40
+ import { createOllamaClient, listModels, pullModel, chatStructured } from '@fgv/ts-extras-ollama';
41
+ import { JsonSchema } from '@fgv/ts-json-base';
42
+
43
+ // Construct a client (defaults to http://127.0.0.1:11434). Returns Result<IOllamaClient>.
44
+ const client = createOllamaClient().orThrow();
45
+
46
+ // Enumerate locally-pulled models with GGUF metadata the /v1 layer can't surface.
47
+ const models = await listModels(client);
48
+ if (models.isSuccess()) {
49
+ for (const m of models.value) {
50
+ console.log(`${m.name} — ${m.details.parameterSize} ${m.details.quantizationLevel}, ${m.size} bytes`);
51
+ }
52
+ }
53
+
54
+ // Pull a model with streamed progress; the Result resolves when the stream terminates.
55
+ const pulled = await pullModel(client, {
56
+ model: 'llama3.1:8b',
57
+ onProgress: (p) => console.log(p.status, p.completed, '/', p.total)
58
+ });
59
+
60
+ // Grammar-constrained structured output — one schema is BOTH the wire `format` and the validator.
61
+ const personSchema = JsonSchema.object({
62
+ name: JsonSchema.string(),
63
+ age: JsonSchema.integer()
64
+ });
65
+
66
+ const structured = await chatStructured(client, {
67
+ model: 'llama3.1:8b',
68
+ messages: [{ role: 'user', content: 'Invent a person as JSON.' }],
69
+ schema: personSchema // T is derived via JsonSchema.Static<typeof personSchema> — no cast
70
+ });
71
+ if (structured.isSuccess()) {
72
+ // structured.value.value is { name: string; age: number }, validated by the same schema.
73
+ console.log(structured.value.value.name, structured.value.value.age);
74
+ }
75
+ ```
76
+
77
+ For **text completion / streaming / tool-use** against the same daemon, use
78
+ `@fgv/ts-extras/ai-assist` (`callProviderCompletion` / `callProviderCompletionStream` /
79
+ `executeClientToolTurn`) with a provider `endpoint` of `http://localhost:11434/v1` — that path is
80
+ not duplicated here.
81
+
82
+ ## Explicitly NOT in scope
83
+
84
+ - Text completion / free-text chat / streaming chat → ai-assist owns it via `/v1`.
85
+ - Browser / CORS path → Node-only at v0.1; a future `@fgv/ts-web-extras-ollama` is its home.
86
+ - Model authoring / publishing (`push`, `create`, `copy`) → use the `ollama` lib directly.
87
+ - `keep_alive` / model-lifecycle policy → pass-through only, no policy applied.
88
+ - Pull-progress UI / rendering → `onProgress` hands raw chunks to the consumer.
89
+ - Multi-host orchestration / connection pooling / retries / backoff → one client = one host.
90
+ - Native embeddings (`embed`) → HELD pending the cross-provider `ai-assist-embeddings` design.
91
+
92
+ For anything not wrapped here, use the `ollama` library directly (with `captureAsyncResult` for your
93
+ own Result wrapping) — the client handle returned by `createOllamaClient` is the upstream instance.
94
+
95
+ ---
96
+
97
+ ## Runtime requirements
98
+
99
+ - **Node.js:** v20 LTS or later.
100
+ - **`ollama`:** ^0.6.0 (peer dependency; bring your own).
101
+ - **`@fgv/ts-utils`:** workspace:* (peer dependency).
102
+
103
+ ## License
104
+
105
+ MIT — same as the parent fgv monorepo.
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3
+ "projectFolder": "..",
4
+ "mainEntryPointFilePath": "<projectFolder>/lib/index.d.ts",
5
+ "bundledPackages": [],
6
+ "compiler": {},
7
+ "apiReport": {
8
+ "enabled": true
9
+ },
10
+ "docModel": {
11
+ "enabled": true
12
+ },
13
+ "dtsRollup": {
14
+ "enabled": true
15
+ },
16
+ "tsdocMetadata": {},
17
+ "messages": {
18
+ "compilerMessageReporting": {
19
+ "default": {
20
+ "logLevel": "warning"
21
+ }
22
+ },
23
+ "extractorMessageReporting": {
24
+ "default": {
25
+ "logLevel": "warning"
26
+ },
27
+ "ae-unresolved-link": {
28
+ "logLevel": "none",
29
+ "addToApiReportFile": true
30
+ }
31
+ },
32
+ "tsdocMessageReporting": {
33
+ "default": {
34
+ "logLevel": "warning"
35
+ }
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "@rushstack/heft-node-rig/profiles/default/config/jest.config.json",
3
+ "coverageThreshold": {
4
+ "global": {
5
+ "branches": 100,
6
+ "functions": 100,
7
+ "lines": 100,
8
+ "statements": 100
9
+ }
10
+ },
11
+ "collectCoverage": true,
12
+ "coverageReporters": ["text", "lcov", "html"]
13
+ }
@@ -0,0 +1,6 @@
1
+ // The "rig.json" file directs tools to look for their config files in an external package.
2
+ // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
3
+ {
4
+ "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
5
+ "rigPackageName": "@fgv/heft-dual-rig"
6
+ }
package/dist/index.js ADDED
@@ -0,0 +1,388 @@
1
+ /**
2
+ * `@fgv/ts-extras-ollama` — Result-integration boundary over the official `ollama` JS library
3
+ * (Node-side).
4
+ *
5
+ * A thin facade that wraps the `ollama` client's native-API calls in `Result<T>` from
6
+ * `@fgv/ts-utils`, mirroring the discipline established by `@fgv/ts-extras-webauthn` and
7
+ * `@fgv/ts-extras-transformers`: one-line `captureResult` / `captureAsyncResult` wrappers around
8
+ * upstream primitives with **no opinionated orchestration** above the boundary.
9
+ *
10
+ * This package owns **exactly and only** the native-Ollama surface that the OpenAI-compatible
11
+ * `/v1` endpoint cannot express: model management (`/api/tags`, `/api/show`, `/api/ps`,
12
+ * `/api/delete`), streamed model pulls (`/api/pull`), and grammar-constrained structured output
13
+ * (`/api/chat` with a full-JSON-schema `format`). The text-completion / streaming / tool-use path
14
+ * is **not** duplicated here — `@fgv/ts-extras/ai-assist` owns it via the `/v1` compat layer
15
+ * (point a provider descriptor's `endpoint` at `http://localhost:11434/v1`).
16
+ *
17
+ * **Explicitly NOT in scope:**
18
+ * - Text completion / free-text chat / streaming chat (ai-assist owns it via `/v1`).
19
+ * - Browser / CORS path (Node-only at v0.1; a future `@fgv/ts-web-extras-ollama` is its home).
20
+ * - Model authoring / publishing (`push`, `create`, `copy`) — use the `ollama` lib directly.
21
+ * - `keep_alive` / model-lifecycle policy — pass-through only, no policy applied.
22
+ * - Pull-progress UI / rendering — `onProgress` hands raw chunks to the consumer.
23
+ * - Multi-host orchestration / connection pooling / retries / backoff — one client = one host.
24
+ *
25
+ * For any of the above, use the `ollama` library directly (with `captureAsyncResult` for your own
26
+ * Result wrapping); the opaque client handle returned by {@link createOllamaClient} is the
27
+ * upstream instance, so nothing this boundary omits is hidden from you.
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
32
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
33
+ var m = o[Symbol.asyncIterator], i;
34
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
35
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
36
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
37
+ };
38
+ import { Ollama } from 'ollama';
39
+ import { captureAsyncResult, captureResult, succeed } from '@fgv/ts-utils';
40
+ /**
41
+ * Constructs an Ollama client. Synchronous (the constructor performs no I/O), so returns
42
+ * `Result<IOllamaClient>` rather than a promise. A malformed `host` (which the upstream
43
+ * constructor rejects) surfaces as `Result.fail`.
44
+ *
45
+ * @param params - Optional {@link ICreateOllamaClientParams}. Omitted fields fall back to the
46
+ * `ollama` library defaults.
47
+ * @returns `Result<IOllamaClient>` wrapping the upstream `Ollama` instance.
48
+ * @public
49
+ */
50
+ export function createOllamaClient(params) {
51
+ // The upstream `Ollama` constructor reads each config field with `?? <default>`, so omitted or
52
+ // `undefined` fields fall back to the library defaults — no per-field guarding needed.
53
+ return captureResult(() => new Ollama(params !== null && params !== void 0 ? params : {}));
54
+ }
55
+ // ─── Wire → fgv normalization (§6.3, Appendix) ──────────────────────────────────
56
+ /**
57
+ * Maps the upstream snake_case `ModelDetails` to the normalized camelCase {@link IOllamaModelDetail}.
58
+ * Upstream types every field as a required `string`, but Ollama omits fields it cannot determine,
59
+ * so individual values may be `undefined` at runtime — the normalized type reflects that by making
60
+ * every field optional.
61
+ */
62
+ function normalizeModelDetail(d) {
63
+ return {
64
+ format: d.format,
65
+ family: d.family,
66
+ families: d.families,
67
+ parameterSize: d.parameter_size,
68
+ quantizationLevel: d.quantization_level,
69
+ parentModel: d.parent_model
70
+ };
71
+ }
72
+ /**
73
+ * Maps a `/api/tags` `ModelResponse` to {@link IOllamaModel}. `modified_at` is normalized to a
74
+ * `Date` via the `Date` constructor, which accepts both the RFC3339 string the wire actually
75
+ * delivers and the `Date` the upstream type (inaccurately) claims.
76
+ */
77
+ function normalizeModel(m) {
78
+ return {
79
+ name: m.name,
80
+ model: m.model,
81
+ size: m.size,
82
+ digest: m.digest,
83
+ details: normalizeModelDetail(m.details),
84
+ modifiedAt: new Date(m.modified_at)
85
+ };
86
+ }
87
+ /**
88
+ * Maps a `/api/ps` `ModelResponse` to {@link IOllamaRunningModel}. `/api/ps` returns `expires_at`
89
+ * and `size_vram` in place of `modified_at`.
90
+ */
91
+ function normalizeRunningModel(m) {
92
+ return {
93
+ name: m.name,
94
+ model: m.model,
95
+ size: m.size,
96
+ digest: m.digest,
97
+ details: normalizeModelDetail(m.details),
98
+ expiresAt: new Date(m.expires_at),
99
+ sizeVram: m.size_vram
100
+ };
101
+ }
102
+ /**
103
+ * Normalizes the `model_info` metadata bag. The upstream type annotates it as `Map<string, any>`,
104
+ * but the lib returns `response.json()` verbatim, so the wire delivers a plain JSON object (or
105
+ * omits the field). The single `as unknown as` bridge reconciles the inaccurate upstream annotation
106
+ * with the actual JSON shape — there is no untyped input to validate here.
107
+ */
108
+ function normalizeModelInfo(raw) {
109
+ if (raw === undefined) {
110
+ return undefined;
111
+ }
112
+ return raw;
113
+ }
114
+ /**
115
+ * Maps a `/api/show` `ShowResponse` to {@link IOllamaModelInfo}. The remaining `ShowResponse`
116
+ * fields (`license`, `system`, `messages`, `modified_at`, `projector_info`) are intentionally not
117
+ * carried through — they are out of scope for the v0.1 surface.
118
+ */
119
+ function normalizeShowResponse(r) {
120
+ return {
121
+ modelfile: r.modelfile,
122
+ parameters: r.parameters,
123
+ template: r.template,
124
+ details: normalizeModelDetail(r.details),
125
+ modelInfo: normalizeModelInfo(r.model_info),
126
+ capabilities: r.capabilities
127
+ };
128
+ }
129
+ // ─── Model management primitives (§3.3) ─────────────────────────────────────────
130
+ /**
131
+ * Lists models in the local store (`GET /api/tags`). Each entry is normalized to
132
+ * {@link IOllamaModel} (camelCase, `modifiedAt` as a `Date`).
133
+ *
134
+ * @param client - A client from {@link createOllamaClient}.
135
+ * @returns `Promise<Result<ReadonlyArray<IOllamaModel>>>`; upstream / transport errors are captured
136
+ * as `Failure`.
137
+ * @public
138
+ */
139
+ export async function listModels(client) {
140
+ return captureAsyncResult(async () => {
141
+ const response = await client.list();
142
+ return response.models.map(normalizeModel);
143
+ });
144
+ }
145
+ /**
146
+ * Lists models currently loaded into memory (`GET /api/ps`). Each entry is normalized to
147
+ * {@link IOllamaRunningModel} (camelCase, `expiresAt` as a `Date`, `sizeVram` in bytes).
148
+ *
149
+ * @param client - A client from {@link createOllamaClient}.
150
+ * @returns `Promise<Result<ReadonlyArray<IOllamaRunningModel>>>`; upstream / transport errors are
151
+ * captured as `Failure`.
152
+ * @public
153
+ */
154
+ export async function listRunning(client) {
155
+ return captureAsyncResult(async () => {
156
+ const response = await client.ps();
157
+ return response.models.map(normalizeRunningModel);
158
+ });
159
+ }
160
+ /**
161
+ * Full model detail, capabilities, parameters, and template (`POST /api/show`), normalized to
162
+ * {@link IOllamaModelInfo}.
163
+ *
164
+ * @param client - A client from {@link createOllamaClient}.
165
+ * @param model - The model name to inspect, e.g. `'llama3.1:8b'`.
166
+ * @param options - Optional flags. `verbose: true` requests the full low-level `model_info` block.
167
+ * @returns `Promise<Result<IOllamaModelInfo>>`; upstream / transport errors (e.g. unknown model)
168
+ * are captured as `Failure`.
169
+ * @public
170
+ */
171
+ export async function showModel(client, model, options) {
172
+ return captureAsyncResult(async () => {
173
+ // `verbose` is a valid `/api/show` body field that the lib forwards verbatim, but the upstream
174
+ // `ShowRequest` type omits it; building it as a non-literal object passes it through without a
175
+ // cast (and omits the key entirely when no flag was supplied).
176
+ const request = Object.assign({ model }, ((options === null || options === void 0 ? void 0 : options.verbose) !== undefined && { verbose: options.verbose }));
177
+ const response = await client.show(request);
178
+ return normalizeShowResponse(response);
179
+ });
180
+ }
181
+ /**
182
+ * Deletes a model from the local store (`DELETE /api/delete`). Returns a meaningful
183
+ * {@link IOllamaDeleteResult} rather than `Result<void>`.
184
+ *
185
+ * @param client - A client from {@link createOllamaClient}.
186
+ * @param model - The model name to delete.
187
+ * @returns `Promise<Result<IOllamaDeleteResult>>`; a missing model or transport error surfaces as
188
+ * `Failure`.
189
+ * @public
190
+ */
191
+ export async function deleteModel(client, model) {
192
+ return captureAsyncResult(async () => {
193
+ await client.delete({ model });
194
+ return { model, deleted: true };
195
+ });
196
+ }
197
+ /**
198
+ * Maps an upstream `ProgressResponse` chunk to {@link IOllamaPullProgress}. The upstream type marks
199
+ * every field required, but the layer fields are omitted (and thus `undefined` at runtime) on the
200
+ * non-transfer phases — a direct structural copy preserves that, matching the optional fields.
201
+ */
202
+ function normalizePullProgress(c) {
203
+ return {
204
+ status: c.status,
205
+ digest: c.digest,
206
+ total: c.total,
207
+ completed: c.completed
208
+ };
209
+ }
210
+ /**
211
+ * Pulls a model with streamed progress (`POST /api/pull`). Drives the upstream JSON-lines stream
212
+ * internally, invoking `params.onProgress` once per chunk, and resolves the `Result` once the
213
+ * stream terminates. Any upstream error — connection drop, registry 404, abort, or a throwing
214
+ * `onProgress` — becomes a single `Failure`; success returns a meaningful {@link IOllamaPullResult}
215
+ * rather than `Result<void>`.
216
+ *
217
+ * @param client - A client from {@link createOllamaClient}.
218
+ * @param params - {@link IPullModelParams} — the model, optional `insecure` flag, optional
219
+ * `onProgress` callback, and optional `AbortSignal`.
220
+ * @returns `Promise<Result<IOllamaPullResult>>`.
221
+ * @public
222
+ */
223
+ export async function pullModel(client, params) {
224
+ return captureAsyncResult(async () => {
225
+ var _a, e_1, _b, _c;
226
+ var _d, _e;
227
+ const request = Object.assign({ model: params.model, stream: true }, (params.insecure !== undefined && { insecure: params.insecure }));
228
+ const iterator = await client.pull(request);
229
+ // Wire the abort signal to the upstream iterator's abort path. An already-aborted signal
230
+ // cancels immediately; otherwise abort on first fire (and clean the listener up in `finally`).
231
+ const abortListener = () => iterator.abort();
232
+ if (params.signal !== undefined) {
233
+ if (params.signal.aborted) {
234
+ iterator.abort();
235
+ }
236
+ else {
237
+ params.signal.addEventListener('abort', abortListener, { once: true });
238
+ }
239
+ }
240
+ let chunkCount = 0;
241
+ let finalStatus = '';
242
+ try {
243
+ try {
244
+ for (var _f = true, iterator_1 = __asyncValues(iterator), iterator_1_1; iterator_1_1 = await iterator_1.next(), _a = iterator_1_1.done, !_a; _f = true) {
245
+ _c = iterator_1_1.value;
246
+ _f = false;
247
+ const chunk = _c;
248
+ (_d = params.onProgress) === null || _d === void 0 ? void 0 : _d.call(params, normalizePullProgress(chunk));
249
+ chunkCount += 1;
250
+ finalStatus = chunk.status;
251
+ }
252
+ }
253
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
254
+ finally {
255
+ try {
256
+ if (!_f && !_a && (_b = iterator_1.return)) await _b.call(iterator_1);
257
+ }
258
+ finally { if (e_1) throw e_1.error; }
259
+ }
260
+ }
261
+ finally {
262
+ (_e = params.signal) === null || _e === void 0 ? void 0 : _e.removeEventListener('abort', abortListener);
263
+ }
264
+ return { model: params.model, finalStatus, chunkCount };
265
+ });
266
+ }
267
+ /**
268
+ * Recursively removes the JSON-Schema keywords (`$schema`, `additionalProperties`) — *if present*
269
+ * — that Ollama's `format` does not need. `JsonSchema.object(...).toJson()` emits
270
+ * `additionalProperties: false` on object nodes; `$schema` is not added by default but may appear
271
+ * on a runtime-discovered schema (`JsonSchema.fromJson(raw)`). Mirrors the ai-assist Gemini
272
+ * precedent (`toGeminiParameterSchema`): the keywords are removed only where they appear as schema
273
+ * *keywords* (siblings of `type`/`properties`/…). Inside a `properties` map the keys are
274
+ * user-defined property names, not keywords, so they are preserved verbatim while each property's
275
+ * subschema value is still recursively sanitized. Stripping is infallible, so it returns a plain
276
+ * value rather than a `Result`.
277
+ */
278
+ function sanitizeFormatSchema(schema) {
279
+ if (Array.isArray(schema)) {
280
+ return schema.map(sanitizeFormatSchema);
281
+ }
282
+ if (schema !== null && typeof schema === 'object') {
283
+ const out = {};
284
+ for (const [key, value] of Object.entries(schema)) {
285
+ if (key === '$schema' || key === 'additionalProperties') {
286
+ continue;
287
+ }
288
+ if (key === 'properties' && value !== null && typeof value === 'object' && !Array.isArray(value)) {
289
+ const properties = {};
290
+ for (const [name, propSchema] of Object.entries(value)) {
291
+ properties[name] = sanitizeFormatSchema(propSchema);
292
+ }
293
+ out[key] = properties;
294
+ }
295
+ else {
296
+ out[key] = sanitizeFormatSchema(value);
297
+ }
298
+ }
299
+ return out;
300
+ }
301
+ return schema;
302
+ }
303
+ /**
304
+ * Maps an {@link IOllamaChatMessage} to the upstream `Message` wire shape, copying the optional
305
+ * base64 `images` array only when present.
306
+ */
307
+ function toWireMessage(message) {
308
+ return Object.assign({ role: message.role, content: message.content }, (message.images !== undefined && { images: [...message.images] }));
309
+ }
310
+ /**
311
+ * Single-turn grammar-constrained structured chat (`POST /api/chat` with `format` = full JSON
312
+ * schema). The fidelity win over ai-assist's prompt-and-parse path is **grammar-constrained
313
+ * sampling**: Ollama restricts the token sampler to the supplied schema, so the response is
314
+ * structurally guaranteed to match.
315
+ *
316
+ * The schema is the single source of truth: `schema.toJson()` (draft-07-sanitized) is sent as the
317
+ * wire `format`, and the same `schema.validate()` checks the reply — they cannot drift. `T` flows
318
+ * end-to-end from `JsonSchema.Static<typeof schema>`; no caller-supplied `T`, no cast.
319
+ *
320
+ * Runs over the streaming chat path internally, assembling the full document from the stream
321
+ * chunks and validating it whole — partial JSON has no validation value. (The design §4 sketch
322
+ * said `stream: false`, but the locked OQ-3 amendment requires an `AbortSignal`, and the `ollama`
323
+ * lib only threads a signal on the streaming path — its non-streaming request has no signal
324
+ * plumbing. The streaming `AbortableAsyncIterator.abort()` is "the mechanism that already exists
325
+ * for `pullModel`".) Any upstream error, abort, parse failure, or validation failure surfaces as a
326
+ * single `Failure` with context.
327
+ *
328
+ * @param client - A client from {@link createOllamaClient}.
329
+ * @param params - {@link IChatStructuredParams} — model, messages, the `JsonSchema` schema, and
330
+ * optional `options` / `keepAlive` / `signal`.
331
+ * @returns `Promise<Result<IOllamaChatStructuredResult<T>>>`.
332
+ * @public
333
+ */
334
+ export async function chatStructured(client, params) {
335
+ const accumulated = await captureAsyncResult(async () => {
336
+ var _a, e_2, _b, _c;
337
+ var _d;
338
+ const wireFormat = sanitizeFormatSchema(params.schema.toJson());
339
+ const request = Object.assign(Object.assign({ model: params.model, messages: params.messages.map(toWireMessage),
340
+ // The wire schema accepts a plain object; the lib types `format` as `string | object`.
341
+ format: wireFormat, stream: true }, (params.options !== undefined && { options: params.options })), (params.keepAlive !== undefined && { keep_alive: params.keepAlive }));
342
+ const iterator = await client.chat(request);
343
+ const abortListener = () => iterator.abort();
344
+ if (params.signal !== undefined) {
345
+ if (params.signal.aborted) {
346
+ iterator.abort();
347
+ }
348
+ else {
349
+ params.signal.addEventListener('abort', abortListener, { once: true });
350
+ }
351
+ }
352
+ let raw = '';
353
+ let model = params.model;
354
+ let doneReason;
355
+ try {
356
+ try {
357
+ for (var _e = true, iterator_2 = __asyncValues(iterator), iterator_2_1; iterator_2_1 = await iterator_2.next(), _a = iterator_2_1.done, !_a; _e = true) {
358
+ _c = iterator_2_1.value;
359
+ _e = false;
360
+ const chunk = _c;
361
+ raw += chunk.message.content;
362
+ model = chunk.model;
363
+ if (chunk.done) {
364
+ doneReason = chunk.done_reason;
365
+ }
366
+ }
367
+ }
368
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
369
+ finally {
370
+ try {
371
+ if (!_e && !_a && (_b = iterator_2.return)) await _b.call(iterator_2);
372
+ }
373
+ finally { if (e_2) throw e_2.error; }
374
+ }
375
+ }
376
+ finally {
377
+ (_d = params.signal) === null || _d === void 0 ? void 0 : _d.removeEventListener('abort', abortListener);
378
+ }
379
+ return { raw, model, doneReason };
380
+ });
381
+ return accumulated.onSuccess(({ raw, model, doneReason }) => captureResult(() => JSON.parse(raw))
382
+ .withErrorFormat((message) => `chatStructured(${params.model}): response was not valid JSON: ${message}`)
383
+ .onSuccess((parsed) => params.schema
384
+ .validate(parsed)
385
+ .withErrorFormat((message) => `chatStructured(${params.model}): response failed schema validation: ${message}`)
386
+ .onSuccess((value) => succeed({ value, raw, model, doneReason }))));
387
+ }
388
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `@fgv/ts-extras-ollama` — Result-integration boundary over the official `ollama` JS library\n * (Node-side).\n *\n * A thin facade that wraps the `ollama` client's native-API calls in `Result<T>` from\n * `@fgv/ts-utils`, mirroring the discipline established by `@fgv/ts-extras-webauthn` and\n * `@fgv/ts-extras-transformers`: one-line `captureResult` / `captureAsyncResult` wrappers around\n * upstream primitives with **no opinionated orchestration** above the boundary.\n *\n * This package owns **exactly and only** the native-Ollama surface that the OpenAI-compatible\n * `/v1` endpoint cannot express: model management (`/api/tags`, `/api/show`, `/api/ps`,\n * `/api/delete`), streamed model pulls (`/api/pull`), and grammar-constrained structured output\n * (`/api/chat` with a full-JSON-schema `format`). The text-completion / streaming / tool-use path\n * is **not** duplicated here — `@fgv/ts-extras/ai-assist` owns it via the `/v1` compat layer\n * (point a provider descriptor's `endpoint` at `http://localhost:11434/v1`).\n *\n * **Explicitly NOT in scope:**\n * - Text completion / free-text chat / streaming chat (ai-assist owns it via `/v1`).\n * - Browser / CORS path (Node-only at v0.1; a future `@fgv/ts-web-extras-ollama` is its home).\n * - Model authoring / publishing (`push`, `create`, `copy`) — use the `ollama` lib directly.\n * - `keep_alive` / model-lifecycle policy — pass-through only, no policy applied.\n * - Pull-progress UI / rendering — `onProgress` hands raw chunks to the consumer.\n * - Multi-host orchestration / connection pooling / retries / backoff — one client = one host.\n *\n * For any of the above, use the `ollama` library directly (with `captureAsyncResult` for your own\n * Result wrapping); the opaque client handle returned by {@link createOllamaClient} is the\n * upstream instance, so nothing this boundary omits is hidden from you.\n *\n * @packageDocumentation\n */\n\nimport {\n Ollama,\n type ChatRequest,\n type ListResponse,\n type Message,\n type ModelResponse,\n type ModelDetails,\n type Options,\n type ProgressResponse,\n type ShowResponse\n} from 'ollama';\nimport { captureAsyncResult, captureResult, succeed, type Result } from '@fgv/ts-utils';\nimport { type JsonObject, type JsonValue, type JsonSchema } from '@fgv/ts-json-base';\n\n/**\n * Construction parameters for an Ollama client. All optional; defaults match the `ollama` JS\n * library (host `http://127.0.0.1:11434`).\n * @public\n */\nexport interface ICreateOllamaClientParams {\n /** Server address. Defaults to `http://127.0.0.1:11434`. */\n readonly host?: string;\n /**\n * Custom `fetch` implementation. Injecting a fetch is the primary unit-test seam and the hook\n * for a future proxy / browser path.\n */\n readonly fetch?: typeof fetch;\n /** Custom headers included on every request (e.g. an auth token for a guarded sidecar). */\n readonly headers?: Record<string, string>;\n}\n\n/**\n * Opaque client handle. Re-export of the upstream `ollama` `Ollama` instance type — consumers\n * retain full access to the upstream API for anything this boundary does not wrap.\n * @public\n */\nexport type IOllamaClient = Ollama;\n\n/**\n * Constructs an Ollama client. Synchronous (the constructor performs no I/O), so returns\n * `Result<IOllamaClient>` rather than a promise. A malformed `host` (which the upstream\n * constructor rejects) surfaces as `Result.fail`.\n *\n * @param params - Optional {@link ICreateOllamaClientParams}. Omitted fields fall back to the\n * `ollama` library defaults.\n * @returns `Result<IOllamaClient>` wrapping the upstream `Ollama` instance.\n * @public\n */\nexport function createOllamaClient(params?: ICreateOllamaClientParams): Result<IOllamaClient> {\n // The upstream `Ollama` constructor reads each config field with `?? <default>`, so omitted or\n // `undefined` fields fall back to the library defaults — no per-field guarding needed.\n return captureResult(() => new Ollama(params ?? {}));\n}\n\n// ─── Shared normalized model types (§3.2) ───────────────────────────────────────\n\n/**\n * GGUF / model detail block, common to `/api/tags`, `/api/ps`, and `/api/show`. All fields\n * optional — Ollama omits any it cannot determine.\n * @public\n */\nexport interface IOllamaModelDetail {\n /** Storage format, e.g. `'gguf'`. */\n readonly format?: string;\n /** Primary architecture family, e.g. `'llama'`. */\n readonly family?: string;\n /** All architecture families this model belongs to. */\n readonly families?: ReadonlyArray<string>;\n /** Human-readable parameter count, e.g. `'8.0B'`. */\n readonly parameterSize?: string;\n /** Quantization level, e.g. `'Q4_0'`. */\n readonly quantizationLevel?: string;\n /** Parent model for fine-tunes / derived models. */\n readonly parentModel?: string;\n}\n\n/**\n * Fields common to a locally-stored model and a running model.\n * @public\n */\nexport interface IOllamaModelBase {\n /** Tagged name, e.g. `'llama3.1:8b'`. */\n readonly name: string;\n /** Underlying model id (often equal to `name`). */\n readonly model: string;\n /** On-disk size in bytes. */\n readonly size: number;\n /** SHA-256 manifest digest. */\n readonly digest: string;\n /** GGUF / architecture detail. */\n readonly details: IOllamaModelDetail;\n}\n\n/**\n * A model present in the local store (`/api/tags`).\n * @public\n */\nexport interface IOllamaModel extends IOllamaModelBase {\n /** Last-modified timestamp, parsed from the RFC3339 wire value. */\n readonly modifiedAt: Date;\n}\n\n/**\n * A model currently loaded into memory (`/api/ps`). Note `/api/ps` does NOT return\n * `modified_at`, so this does not extend {@link IOllamaModel}.\n * @public\n */\nexport interface IOllamaRunningModel extends IOllamaModelBase {\n /** When the model will be unloaded, parsed from the RFC3339 wire value. */\n readonly expiresAt: Date;\n /** Bytes resident in VRAM (0 when fully on CPU). */\n readonly sizeVram: number;\n}\n\n/**\n * Full model detail, capabilities, parameters, and template (`POST /api/show`).\n * @public\n */\nexport interface IOllamaModelInfo {\n /** The Modelfile source, when returned. */\n readonly modelfile?: string;\n /** Default parameters string. */\n readonly parameters?: string;\n /** The prompt template. */\n readonly template?: string;\n /** GGUF / architecture detail. */\n readonly details: IOllamaModelDetail;\n /** Low-level model metadata key-value map (`model_info`). */\n readonly modelInfo?: Readonly<Record<string, JsonValue>>;\n /** Declared capabilities, e.g. `['completion', 'tools', 'vision', 'thinking']`. */\n readonly capabilities?: ReadonlyArray<string>;\n}\n\n/**\n * Result of a delete. Returns a meaningful value rather than `Result<void>` (per repo standards —\n * `Result<void>` is an anti-pattern).\n * @public\n */\nexport interface IOllamaDeleteResult {\n /** The model that was deleted. */\n readonly model: string;\n /** Always `true` — a failed delete surfaces as `Result.fail`, never `deleted: false`. */\n readonly deleted: true;\n}\n\n// ─── Wire → fgv normalization (§6.3, Appendix) ──────────────────────────────────\n\n/**\n * Maps the upstream snake_case `ModelDetails` to the normalized camelCase {@link IOllamaModelDetail}.\n * Upstream types every field as a required `string`, but Ollama omits fields it cannot determine,\n * so individual values may be `undefined` at runtime — the normalized type reflects that by making\n * every field optional.\n */\nfunction normalizeModelDetail(d: ModelDetails): IOllamaModelDetail {\n return {\n format: d.format,\n family: d.family,\n families: d.families,\n parameterSize: d.parameter_size,\n quantizationLevel: d.quantization_level,\n parentModel: d.parent_model\n };\n}\n\n/**\n * Maps a `/api/tags` `ModelResponse` to {@link IOllamaModel}. `modified_at` is normalized to a\n * `Date` via the `Date` constructor, which accepts both the RFC3339 string the wire actually\n * delivers and the `Date` the upstream type (inaccurately) claims.\n */\nfunction normalizeModel(m: ModelResponse): IOllamaModel {\n return {\n name: m.name,\n model: m.model,\n size: m.size,\n digest: m.digest,\n details: normalizeModelDetail(m.details),\n modifiedAt: new Date(m.modified_at)\n };\n}\n\n/**\n * Maps a `/api/ps` `ModelResponse` to {@link IOllamaRunningModel}. `/api/ps` returns `expires_at`\n * and `size_vram` in place of `modified_at`.\n */\nfunction normalizeRunningModel(m: ModelResponse): IOllamaRunningModel {\n return {\n name: m.name,\n model: m.model,\n size: m.size,\n digest: m.digest,\n details: normalizeModelDetail(m.details),\n expiresAt: new Date(m.expires_at),\n sizeVram: m.size_vram\n };\n}\n\n/**\n * Normalizes the `model_info` metadata bag. The upstream type annotates it as `Map<string, any>`,\n * but the lib returns `response.json()` verbatim, so the wire delivers a plain JSON object (or\n * omits the field). The single `as unknown as` bridge reconciles the inaccurate upstream annotation\n * with the actual JSON shape — there is no untyped input to validate here.\n */\nfunction normalizeModelInfo(\n raw: ShowResponse['model_info']\n): Readonly<Record<string, JsonValue>> | undefined {\n if (raw === undefined) {\n return undefined;\n }\n return raw as unknown as Readonly<Record<string, JsonValue>>;\n}\n\n/**\n * Maps a `/api/show` `ShowResponse` to {@link IOllamaModelInfo}. The remaining `ShowResponse`\n * fields (`license`, `system`, `messages`, `modified_at`, `projector_info`) are intentionally not\n * carried through — they are out of scope for the v0.1 surface.\n */\nfunction normalizeShowResponse(r: ShowResponse): IOllamaModelInfo {\n return {\n modelfile: r.modelfile,\n parameters: r.parameters,\n template: r.template,\n details: normalizeModelDetail(r.details),\n modelInfo: normalizeModelInfo(r.model_info),\n capabilities: r.capabilities\n };\n}\n\n// ─── Model management primitives (§3.3) ─────────────────────────────────────────\n\n/**\n * Lists models in the local store (`GET /api/tags`). Each entry is normalized to\n * {@link IOllamaModel} (camelCase, `modifiedAt` as a `Date`).\n *\n * @param client - A client from {@link createOllamaClient}.\n * @returns `Promise<Result<ReadonlyArray<IOllamaModel>>>`; upstream / transport errors are captured\n * as `Failure`.\n * @public\n */\nexport async function listModels(client: IOllamaClient): Promise<Result<ReadonlyArray<IOllamaModel>>> {\n return captureAsyncResult(async () => {\n const response: ListResponse = await client.list();\n return response.models.map(normalizeModel);\n });\n}\n\n/**\n * Lists models currently loaded into memory (`GET /api/ps`). Each entry is normalized to\n * {@link IOllamaRunningModel} (camelCase, `expiresAt` as a `Date`, `sizeVram` in bytes).\n *\n * @param client - A client from {@link createOllamaClient}.\n * @returns `Promise<Result<ReadonlyArray<IOllamaRunningModel>>>`; upstream / transport errors are\n * captured as `Failure`.\n * @public\n */\nexport async function listRunning(\n client: IOllamaClient\n): Promise<Result<ReadonlyArray<IOllamaRunningModel>>> {\n return captureAsyncResult(async () => {\n const response: ListResponse = await client.ps();\n return response.models.map(normalizeRunningModel);\n });\n}\n\n/**\n * Full model detail, capabilities, parameters, and template (`POST /api/show`), normalized to\n * {@link IOllamaModelInfo}.\n *\n * @param client - A client from {@link createOllamaClient}.\n * @param model - The model name to inspect, e.g. `'llama3.1:8b'`.\n * @param options - Optional flags. `verbose: true` requests the full low-level `model_info` block.\n * @returns `Promise<Result<IOllamaModelInfo>>`; upstream / transport errors (e.g. unknown model)\n * are captured as `Failure`.\n * @public\n */\nexport async function showModel(\n client: IOllamaClient,\n model: string,\n options?: { readonly verbose?: boolean }\n): Promise<Result<IOllamaModelInfo>> {\n return captureAsyncResult(async () => {\n // `verbose` is a valid `/api/show` body field that the lib forwards verbatim, but the upstream\n // `ShowRequest` type omits it; building it as a non-literal object passes it through without a\n // cast (and omits the key entirely when no flag was supplied).\n const request = { model, ...(options?.verbose !== undefined && { verbose: options.verbose }) };\n const response: ShowResponse = await client.show(request);\n return normalizeShowResponse(response);\n });\n}\n\n/**\n * Deletes a model from the local store (`DELETE /api/delete`). Returns a meaningful\n * {@link IOllamaDeleteResult} rather than `Result<void>`.\n *\n * @param client - A client from {@link createOllamaClient}.\n * @param model - The model name to delete.\n * @returns `Promise<Result<IOllamaDeleteResult>>`; a missing model or transport error surfaces as\n * `Failure`.\n * @public\n */\nexport async function deleteModel(\n client: IOllamaClient,\n model: string\n): Promise<Result<IOllamaDeleteResult>> {\n return captureAsyncResult(async () => {\n await client.delete({ model });\n return { model, deleted: true };\n });\n}\n\n// ─── Pull with streamed progress (§3.4, §5) ─────────────────────────────────────\n\n/**\n * A single progress chunk from a streamed pull (`/api/pull`). Ollama emits a JSON-lines stream;\n * each line is one of these. Fields map 1:1 from the upstream `ProgressResponse`; the layer fields\n * (`digest`/`total`/`completed`) are `undefined` on the manifest / verify / write phases.\n * @public\n */\nexport interface IOllamaPullProgress {\n /**\n * Phase string, e.g. `'pulling manifest'`, `'pulling <digest>'`, `'verifying sha256 digest'`,\n * `'writing manifest'`, `'success'`.\n */\n readonly status: string;\n /** Layer digest being transferred (present during `'pulling <digest>'` phases). */\n readonly digest?: string;\n /** Total bytes for the current layer, when known. */\n readonly total?: number;\n /** Bytes transferred so far for the current layer, when known. */\n readonly completed?: number;\n}\n\n/**\n * Terminal result of a completed pull. Returns a meaningful value (not `Result<void>`): the final\n * status plus a count of progress chunks observed.\n * @public\n */\nexport interface IOllamaPullResult {\n /** The model that was pulled. */\n readonly model: string;\n /** The `status` of the final chunk, normally `'success'`. */\n readonly finalStatus: string;\n /** Number of progress chunks observed (useful for tests and diagnostics). */\n readonly chunkCount: number;\n}\n\n/**\n * Parameters for {@link pullModel}.\n * @public\n */\nexport interface IPullModelParams {\n /** Model to pull, e.g. `'llama3.1:8b'`. */\n readonly model: string;\n /** Allow pulling from insecure (non-TLS) registries. */\n readonly insecure?: boolean;\n /**\n * Progress callback, invoked once per JSON-lines chunk as it arrives. The `Result` returned by\n * {@link pullModel} resolves only after the stream terminates; progress is surfaced here in the\n * interim. A throw from this callback fails the whole pull (it runs inside the captured loop).\n */\n readonly onProgress?: (progress: IOllamaPullProgress) => void;\n /**\n * Abort signal. Cancels the in-flight pull; the upstream stream throws an `AbortError`, which\n * surfaces as `Result.fail`. An already-aborted signal cancels immediately.\n */\n readonly signal?: AbortSignal;\n}\n\n/**\n * Maps an upstream `ProgressResponse` chunk to {@link IOllamaPullProgress}. The upstream type marks\n * every field required, but the layer fields are omitted (and thus `undefined` at runtime) on the\n * non-transfer phases — a direct structural copy preserves that, matching the optional fields.\n */\nfunction normalizePullProgress(c: ProgressResponse): IOllamaPullProgress {\n return {\n status: c.status,\n digest: c.digest,\n total: c.total,\n completed: c.completed\n };\n}\n\n/**\n * Pulls a model with streamed progress (`POST /api/pull`). Drives the upstream JSON-lines stream\n * internally, invoking `params.onProgress` once per chunk, and resolves the `Result` once the\n * stream terminates. Any upstream error — connection drop, registry 404, abort, or a throwing\n * `onProgress` — becomes a single `Failure`; success returns a meaningful {@link IOllamaPullResult}\n * rather than `Result<void>`.\n *\n * @param client - A client from {@link createOllamaClient}.\n * @param params - {@link IPullModelParams} — the model, optional `insecure` flag, optional\n * `onProgress` callback, and optional `AbortSignal`.\n * @returns `Promise<Result<IOllamaPullResult>>`.\n * @public\n */\nexport async function pullModel(\n client: IOllamaClient,\n params: IPullModelParams\n): Promise<Result<IOllamaPullResult>> {\n return captureAsyncResult(async () => {\n const request = {\n model: params.model,\n stream: true as const,\n ...(params.insecure !== undefined && { insecure: params.insecure })\n };\n const iterator = await client.pull(request);\n\n // Wire the abort signal to the upstream iterator's abort path. An already-aborted signal\n // cancels immediately; otherwise abort on first fire (and clean the listener up in `finally`).\n const abortListener = (): void => iterator.abort();\n if (params.signal !== undefined) {\n if (params.signal.aborted) {\n iterator.abort();\n } else {\n params.signal.addEventListener('abort', abortListener, { once: true });\n }\n }\n\n let chunkCount = 0;\n let finalStatus = '';\n try {\n for await (const chunk of iterator) {\n params.onProgress?.(normalizePullProgress(chunk));\n chunkCount += 1;\n finalStatus = chunk.status;\n }\n } finally {\n params.signal?.removeEventListener('abort', abortListener);\n }\n\n return { model: params.model, finalStatus, chunkCount };\n });\n}\n\n// ─── Grammar-constrained structured output (§3.5, §4) ───────────────────────────\n\n/**\n * A chat message in Ollama native shape.\n * @public\n */\nexport interface IOllamaChatMessage {\n /** The message role. */\n readonly role: 'system' | 'user' | 'assistant' | 'tool';\n /** The message text. */\n readonly content: string;\n /** Base64 image data for vision models (no `data:` prefix). */\n readonly images?: ReadonlyArray<string>;\n}\n\n/**\n * Result of a grammar-constrained structured chat. Carries both the validated typed value and the\n * raw JSON text the model emitted, for diagnostics.\n * @public\n */\nexport interface IOllamaChatStructuredResult<T> {\n /** The parsed-and-validated value (validated by the same schema sent on the wire). */\n readonly value: T;\n /** The raw `message.content` JSON string the model emitted (assembled across stream chunks). */\n readonly raw: string;\n /** The model that produced the response. */\n readonly model: string;\n /** Provider-reported finish reason, when present. */\n readonly doneReason?: string;\n}\n\n/**\n * Parameters for {@link chatStructured}.\n * @public\n */\nexport interface IChatStructuredParams<T> {\n /** The model to chat with, e.g. `'llama3.1:8b'`. */\n readonly model: string;\n /** The conversation so far. */\n readonly messages: ReadonlyArray<IOllamaChatMessage>;\n /**\n * The single source of truth for both the wire schema (`schema.toJson()`, sent as the native\n * `format` field) and the result validator (`schema.validate()`). Author with\n * `JsonSchema.object(...)` from `@fgv/ts-json-base`; `T` is derived via\n * `JsonSchema.Static<typeof schema>`. A runtime-discovered (MCP) schema parsed via\n * `JsonSchema.fromJson(raw)` also works — its `T` is `JsonValue`.\n */\n readonly schema: JsonSchema.ISchemaValidator<T>;\n /** Native Ollama `options` (temperature, num_ctx, seed, …). Passed verbatim. */\n readonly options?: Readonly<Record<string, JsonValue>>;\n /** Native `keep_alive` (duration string or seconds). Passed verbatim — no policy applied. */\n readonly keepAlive?: string | number;\n /**\n * Abort signal. Cancels the in-flight generation; the upstream stream throws an `AbortError`,\n * which surfaces as `Result.fail`. An already-aborted signal cancels immediately.\n */\n readonly signal?: AbortSignal;\n}\n\n/**\n * Recursively removes the JSON-Schema keywords (`$schema`, `additionalProperties`) — *if present*\n * — that Ollama's `format` does not need. `JsonSchema.object(...).toJson()` emits\n * `additionalProperties: false` on object nodes; `$schema` is not added by default but may appear\n * on a runtime-discovered schema (`JsonSchema.fromJson(raw)`). Mirrors the ai-assist Gemini\n * precedent (`toGeminiParameterSchema`): the keywords are removed only where they appear as schema\n * *keywords* (siblings of `type`/`properties`/…). Inside a `properties` map the keys are\n * user-defined property names, not keywords, so they are preserved verbatim while each property's\n * subschema value is still recursively sanitized. Stripping is infallible, so it returns a plain\n * value rather than a `Result`.\n */\nfunction sanitizeFormatSchema(schema: JsonValue): JsonValue {\n if (Array.isArray(schema)) {\n return schema.map(sanitizeFormatSchema);\n }\n if (schema !== null && typeof schema === 'object') {\n const out: JsonObject = {};\n for (const [key, value] of Object.entries(schema)) {\n if (key === '$schema' || key === 'additionalProperties') {\n continue;\n }\n if (key === 'properties' && value !== null && typeof value === 'object' && !Array.isArray(value)) {\n const properties: JsonObject = {};\n for (const [name, propSchema] of Object.entries(value)) {\n properties[name] = sanitizeFormatSchema(propSchema);\n }\n out[key] = properties;\n } else {\n out[key] = sanitizeFormatSchema(value);\n }\n }\n return out;\n }\n return schema;\n}\n\n/**\n * Maps an {@link IOllamaChatMessage} to the upstream `Message` wire shape, copying the optional\n * base64 `images` array only when present.\n */\nfunction toWireMessage(message: IOllamaChatMessage): Message {\n return {\n role: message.role,\n content: message.content,\n ...(message.images !== undefined && { images: [...message.images] })\n };\n}\n\n/**\n * Single-turn grammar-constrained structured chat (`POST /api/chat` with `format` = full JSON\n * schema). The fidelity win over ai-assist's prompt-and-parse path is **grammar-constrained\n * sampling**: Ollama restricts the token sampler to the supplied schema, so the response is\n * structurally guaranteed to match.\n *\n * The schema is the single source of truth: `schema.toJson()` (draft-07-sanitized) is sent as the\n * wire `format`, and the same `schema.validate()` checks the reply — they cannot drift. `T` flows\n * end-to-end from `JsonSchema.Static<typeof schema>`; no caller-supplied `T`, no cast.\n *\n * Runs over the streaming chat path internally, assembling the full document from the stream\n * chunks and validating it whole — partial JSON has no validation value. (The design §4 sketch\n * said `stream: false`, but the locked OQ-3 amendment requires an `AbortSignal`, and the `ollama`\n * lib only threads a signal on the streaming path — its non-streaming request has no signal\n * plumbing. The streaming `AbortableAsyncIterator.abort()` is \"the mechanism that already exists\n * for `pullModel`\".) Any upstream error, abort, parse failure, or validation failure surfaces as a\n * single `Failure` with context.\n *\n * @param client - A client from {@link createOllamaClient}.\n * @param params - {@link IChatStructuredParams} — model, messages, the `JsonSchema` schema, and\n * optional `options` / `keepAlive` / `signal`.\n * @returns `Promise<Result<IOllamaChatStructuredResult<T>>>`.\n * @public\n */\nexport async function chatStructured<T>(\n client: IOllamaClient,\n params: IChatStructuredParams<T>\n): Promise<Result<IOllamaChatStructuredResult<T>>> {\n const accumulated = await captureAsyncResult(async () => {\n const wireFormat = sanitizeFormatSchema(params.schema.toJson());\n const request: ChatRequest & { stream: true } = {\n model: params.model,\n messages: params.messages.map(toWireMessage),\n // The wire schema accepts a plain object; the lib types `format` as `string | object`.\n format: wireFormat as object,\n stream: true,\n // The lib types `options` narrowly as `Partial<Options>`, but the native API accepts an\n // arbitrary option bag forwarded verbatim — bridge the (overly narrow) upstream type.\n ...(params.options !== undefined && { options: params.options as unknown as Partial<Options> }),\n ...(params.keepAlive !== undefined && { keep_alive: params.keepAlive })\n };\n const iterator = await client.chat(request);\n\n const abortListener = (): void => iterator.abort();\n if (params.signal !== undefined) {\n if (params.signal.aborted) {\n iterator.abort();\n } else {\n params.signal.addEventListener('abort', abortListener, { once: true });\n }\n }\n\n let raw = '';\n let model = params.model;\n let doneReason: string | undefined;\n try {\n for await (const chunk of iterator) {\n raw += chunk.message.content;\n model = chunk.model;\n if (chunk.done) {\n doneReason = chunk.done_reason;\n }\n }\n } finally {\n params.signal?.removeEventListener('abort', abortListener);\n }\n return { raw, model, doneReason };\n });\n\n return accumulated.onSuccess(({ raw, model, doneReason }) =>\n captureResult(() => JSON.parse(raw) as unknown)\n .withErrorFormat(\n (message) => `chatStructured(${params.model}): response was not valid JSON: ${message}`\n )\n .onSuccess((parsed) =>\n params.schema\n .validate(parsed)\n .withErrorFormat(\n (message) => `chatStructured(${params.model}): response failed schema validation: ${message}`\n )\n .onSuccess((value) => succeed({ value, raw, model, doneReason }))\n )\n );\n}\n"]}