@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
@@ -0,0 +1,202 @@
1
+ var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
2
+ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
3
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
4
+ var g = generator.apply(thisArg, _arguments || []), i, q = [];
5
+ return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
6
+ function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
7
+ function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
8
+ function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
9
+ function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
10
+ function fulfill(value) { resume("next", value); }
11
+ function reject(value) { resume("throw", value); }
12
+ function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
13
+ };
14
+ import '@fgv/ts-utils-jest';
15
+ import { createOllamaClient, pullModel } from '../../index';
16
+ /**
17
+ * Builds a structural mock of the upstream `AbortableAsyncIterator` returned by `client.pull`.
18
+ * `abort()` flips an internal flag that the generator checks before each chunk — so an abort
19
+ * triggered mid-stream surfaces as a throw on the next iteration, matching the real lib.
20
+ */
21
+ function makePullIterator(chunks, opts) {
22
+ let aborted = false;
23
+ return {
24
+ abort: jest.fn(() => {
25
+ aborted = true;
26
+ }),
27
+ [Symbol.asyncIterator]() {
28
+ return __asyncGenerator(this, arguments, function* _a() {
29
+ for (const chunk of chunks) {
30
+ if (aborted) {
31
+ throw new Error('The operation was aborted');
32
+ }
33
+ yield yield __await(chunk);
34
+ if (opts === null || opts === void 0 ? void 0 : opts.throwMidStream) {
35
+ throw new Error('stream broke mid-pull');
36
+ }
37
+ }
38
+ });
39
+ }
40
+ };
41
+ }
42
+ function mockPullClient(pull) {
43
+ return { pull };
44
+ }
45
+ const happyChunks = [
46
+ { status: 'pulling manifest' },
47
+ { status: 'pulling sha256:abc', digest: 'sha256:abc', total: 1000, completed: 1000 },
48
+ { status: 'success' }
49
+ ];
50
+ describe('pullModel (layer 1 — structural client mock)', () => {
51
+ test('drives the stream, maps each chunk, and resolves with finalStatus + chunkCount', async () => {
52
+ const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));
53
+ const client = mockPullClient(pull);
54
+ const seen = [];
55
+ expect(await pullModel(client, { model: 'llama3.1:8b', onProgress: (p) => seen.push(p) })).toSucceedWith({
56
+ model: 'llama3.1:8b',
57
+ finalStatus: 'success',
58
+ chunkCount: 3
59
+ });
60
+ expect(pull).toHaveBeenCalledWith({ model: 'llama3.1:8b', stream: true });
61
+ expect(seen).toHaveLength(3);
62
+ // Manifest phase: no layer fields.
63
+ expect(seen[0]).toEqual({
64
+ status: 'pulling manifest',
65
+ digest: undefined,
66
+ total: undefined,
67
+ completed: undefined
68
+ });
69
+ // Transfer phase: layer fields present and mapped 1:1.
70
+ expect(seen[1]).toEqual({
71
+ status: 'pulling sha256:abc',
72
+ digest: 'sha256:abc',
73
+ total: 1000,
74
+ completed: 1000
75
+ });
76
+ expect(seen[2].status).toBe('success');
77
+ });
78
+ test('forwards insecure and works without an onProgress callback', async () => {
79
+ const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));
80
+ const client = mockPullClient(pull);
81
+ expect(await pullModel(client, { model: 'llama3.1:8b', insecure: true })).toSucceedWith({
82
+ model: 'llama3.1:8b',
83
+ finalStatus: 'success',
84
+ chunkCount: 3
85
+ });
86
+ expect(pull).toHaveBeenCalledWith({ model: 'llama3.1:8b', stream: true, insecure: true });
87
+ });
88
+ test('fails when the stream errors mid-pull', async () => {
89
+ const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks, { throwMidStream: true }));
90
+ const client = mockPullClient(pull);
91
+ expect(await pullModel(client, { model: 'llama3.1:8b' })).toFailWith(/stream broke mid-pull/);
92
+ });
93
+ test('fails when onProgress throws', async () => {
94
+ const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));
95
+ const client = mockPullClient(pull);
96
+ expect(await pullModel(client, {
97
+ model: 'llama3.1:8b',
98
+ onProgress: () => {
99
+ throw new Error('consumer blew up');
100
+ }
101
+ })).toFailWith(/consumer blew up/);
102
+ });
103
+ test('an already-aborted signal cancels the pull on the first iteration', async () => {
104
+ // The signal is aborted before the loop begins, so the iterator's abort fires up front; the
105
+ // throw surfaces on the first iteration attempt (the mock checks the flag before the first yield).
106
+ const iterator = makePullIterator(happyChunks);
107
+ const pull = jest.fn().mockResolvedValue(iterator);
108
+ const client = mockPullClient(pull);
109
+ const controller = new AbortController();
110
+ controller.abort();
111
+ expect(await pullModel(client, { model: 'llama3.1:8b', signal: controller.signal })).toFailWith(/abort/i);
112
+ expect(iterator.abort).toHaveBeenCalledTimes(1);
113
+ });
114
+ test('a mid-stream abort cancels the pull, removes the listener, and surfaces as failure', async () => {
115
+ const iterator = makePullIterator(happyChunks);
116
+ const pull = jest.fn().mockResolvedValue(iterator);
117
+ const client = mockPullClient(pull);
118
+ const controller = new AbortController();
119
+ const removeSpy = jest.spyOn(controller.signal, 'removeEventListener');
120
+ const seen = [];
121
+ const result = await pullModel(client, {
122
+ model: 'llama3.1:8b',
123
+ signal: controller.signal,
124
+ onProgress: (p) => {
125
+ seen.push(p);
126
+ if (seen.length === 1) {
127
+ controller.abort();
128
+ }
129
+ }
130
+ });
131
+ expect(result).toFailWith(/abort/i);
132
+ expect(iterator.abort).toHaveBeenCalled();
133
+ expect(seen).toHaveLength(1);
134
+ // The `finally` cleanup runs on the failure path too — no listener leak after an abort.
135
+ expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function));
136
+ });
137
+ test('removes the abort listener after a normal completion', async () => {
138
+ const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));
139
+ const client = mockPullClient(pull);
140
+ const controller = new AbortController();
141
+ const removeSpy = jest.spyOn(controller.signal, 'removeEventListener');
142
+ expect(await pullModel(client, { model: 'llama3.1:8b', signal: controller.signal })).toSucceed();
143
+ expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function));
144
+ });
145
+ });
146
+ /**
147
+ * Layer 2 — a real client built with a mock `fetch` whose body is a `ReadableStream` of
148
+ * newline-delimited JSON. This exercises the `ollama` lib's actual JSON-lines stream parsing
149
+ * flowing through `pullModel`'s loop — the framing that layer 1 stubs over.
150
+ */
151
+ function ndjsonResponse(lines) {
152
+ const bytes = new TextEncoder().encode(lines.join('\n') + '\n');
153
+ // Split the payload mid-stream (likely mid-line) into two reads to exercise buffer reassembly.
154
+ const mid = Math.floor(bytes.length / 2);
155
+ const parts = [bytes.slice(0, mid), bytes.slice(mid)];
156
+ let i = 0;
157
+ const stream = new ReadableStream({
158
+ pull(controller) {
159
+ if (i < parts.length) {
160
+ controller.enqueue(parts[i++]);
161
+ }
162
+ else {
163
+ controller.close();
164
+ }
165
+ }
166
+ });
167
+ return new Response(stream, { status: 200 });
168
+ }
169
+ describe('pullModel (layer 2 — fetch-level JSON-lines integration)', () => {
170
+ test('parses a real newline-delimited /api/pull stream through the ollama lib', async () => {
171
+ const lines = [
172
+ JSON.stringify({ status: 'pulling manifest' }),
173
+ JSON.stringify({ status: 'pulling sha256:abc', digest: 'sha256:abc', total: 1000, completed: 1000 }),
174
+ JSON.stringify({ status: 'verifying sha256 digest' }),
175
+ JSON.stringify({ status: 'writing manifest' }),
176
+ JSON.stringify({ status: 'success' })
177
+ ];
178
+ const fetchMock = jest.fn().mockResolvedValue(ndjsonResponse(lines));
179
+ const client = createOllamaClient({ fetch: fetchMock }).orThrow();
180
+ const seen = [];
181
+ expect(await pullModel(client, { model: 'llama3.1:8b', onProgress: (p) => seen.push(p) })).toSucceedAndSatisfy((r) => {
182
+ expect(r.finalStatus).toBe('success');
183
+ expect(r.chunkCount).toBe(5);
184
+ });
185
+ expect(seen.map((s) => s.status)).toEqual([
186
+ 'pulling manifest',
187
+ 'pulling sha256:abc',
188
+ 'verifying sha256 digest',
189
+ 'writing manifest',
190
+ 'success'
191
+ ]);
192
+ expect(seen[1]).toEqual({
193
+ status: 'pulling sha256:abc',
194
+ digest: 'sha256:abc',
195
+ total: 1000,
196
+ completed: 1000
197
+ });
198
+ expect(seen[0].digest).toBeUndefined();
199
+ expect(fetchMock.mock.calls[0][0]).toEqual(expect.stringContaining('/api/pull'));
200
+ });
201
+ });
202
+ //# sourceMappingURL=pullModel.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pullModel.test.js","sourceRoot":"","sources":["../../../src/test/unit/pullModel.test.ts"],"names":[],"mappings":"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","sourcesContent":["import '@fgv/ts-utils-jest';\nimport { type IOllamaClient, type IOllamaPullProgress, createOllamaClient, pullModel } from '../../index';\n\n/** The actual `/api/pull` wire chunk shape — layer fields absent on non-transfer phases. */\ninterface IPullChunk {\n status: string;\n digest?: string;\n total?: number;\n completed?: number;\n}\n\n/**\n * Builds a structural mock of the upstream `AbortableAsyncIterator` returned by `client.pull`.\n * `abort()` flips an internal flag that the generator checks before each chunk — so an abort\n * triggered mid-stream surfaces as a throw on the next iteration, matching the real lib.\n */\nfunction makePullIterator(\n chunks: ReadonlyArray<IPullChunk>,\n opts?: { readonly throwMidStream?: boolean }\n): { abort: jest.Mock } & AsyncIterable<IPullChunk> {\n let aborted = false;\n return {\n abort: jest.fn(() => {\n aborted = true;\n }),\n async *[Symbol.asyncIterator](): AsyncGenerator<IPullChunk> {\n for (const chunk of chunks) {\n if (aborted) {\n throw new Error('The operation was aborted');\n }\n yield chunk;\n if (opts?.throwMidStream) {\n throw new Error('stream broke mid-pull');\n }\n }\n }\n };\n}\n\nfunction mockPullClient(pull: unknown): IOllamaClient {\n return { pull } as unknown as IOllamaClient;\n}\n\nconst happyChunks: ReadonlyArray<IPullChunk> = [\n { status: 'pulling manifest' },\n { status: 'pulling sha256:abc', digest: 'sha256:abc', total: 1000, completed: 1000 },\n { status: 'success' }\n];\n\ndescribe('pullModel (layer 1 — structural client mock)', () => {\n test('drives the stream, maps each chunk, and resolves with finalStatus + chunkCount', async () => {\n const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));\n const client = mockPullClient(pull);\n const seen: IOllamaPullProgress[] = [];\n\n expect(await pullModel(client, { model: 'llama3.1:8b', onProgress: (p) => seen.push(p) })).toSucceedWith({\n model: 'llama3.1:8b',\n finalStatus: 'success',\n chunkCount: 3\n });\n expect(pull).toHaveBeenCalledWith({ model: 'llama3.1:8b', stream: true });\n expect(seen).toHaveLength(3);\n // Manifest phase: no layer fields.\n expect(seen[0]).toEqual({\n status: 'pulling manifest',\n digest: undefined,\n total: undefined,\n completed: undefined\n });\n // Transfer phase: layer fields present and mapped 1:1.\n expect(seen[1]).toEqual({\n status: 'pulling sha256:abc',\n digest: 'sha256:abc',\n total: 1000,\n completed: 1000\n });\n expect(seen[2].status).toBe('success');\n });\n\n test('forwards insecure and works without an onProgress callback', async () => {\n const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));\n const client = mockPullClient(pull);\n\n expect(await pullModel(client, { model: 'llama3.1:8b', insecure: true })).toSucceedWith({\n model: 'llama3.1:8b',\n finalStatus: 'success',\n chunkCount: 3\n });\n expect(pull).toHaveBeenCalledWith({ model: 'llama3.1:8b', stream: true, insecure: true });\n });\n\n test('fails when the stream errors mid-pull', async () => {\n const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks, { throwMidStream: true }));\n const client = mockPullClient(pull);\n\n expect(await pullModel(client, { model: 'llama3.1:8b' })).toFailWith(/stream broke mid-pull/);\n });\n\n test('fails when onProgress throws', async () => {\n const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));\n const client = mockPullClient(pull);\n\n expect(\n await pullModel(client, {\n model: 'llama3.1:8b',\n onProgress: () => {\n throw new Error('consumer blew up');\n }\n })\n ).toFailWith(/consumer blew up/);\n });\n\n test('an already-aborted signal cancels the pull on the first iteration', async () => {\n // The signal is aborted before the loop begins, so the iterator's abort fires up front; the\n // throw surfaces on the first iteration attempt (the mock checks the flag before the first yield).\n const iterator = makePullIterator(happyChunks);\n const pull = jest.fn().mockResolvedValue(iterator);\n const client = mockPullClient(pull);\n const controller = new AbortController();\n controller.abort();\n\n expect(await pullModel(client, { model: 'llama3.1:8b', signal: controller.signal })).toFailWith(/abort/i);\n expect(iterator.abort).toHaveBeenCalledTimes(1);\n });\n\n test('a mid-stream abort cancels the pull, removes the listener, and surfaces as failure', async () => {\n const iterator = makePullIterator(happyChunks);\n const pull = jest.fn().mockResolvedValue(iterator);\n const client = mockPullClient(pull);\n const controller = new AbortController();\n const removeSpy = jest.spyOn(controller.signal, 'removeEventListener');\n const seen: IOllamaPullProgress[] = [];\n\n const result = await pullModel(client, {\n model: 'llama3.1:8b',\n signal: controller.signal,\n onProgress: (p) => {\n seen.push(p);\n if (seen.length === 1) {\n controller.abort();\n }\n }\n });\n expect(result).toFailWith(/abort/i);\n expect(iterator.abort).toHaveBeenCalled();\n expect(seen).toHaveLength(1);\n // The `finally` cleanup runs on the failure path too — no listener leak after an abort.\n expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function));\n });\n\n test('removes the abort listener after a normal completion', async () => {\n const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));\n const client = mockPullClient(pull);\n const controller = new AbortController();\n const removeSpy = jest.spyOn(controller.signal, 'removeEventListener');\n\n expect(await pullModel(client, { model: 'llama3.1:8b', signal: controller.signal })).toSucceed();\n expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function));\n });\n});\n\n/**\n * Layer 2 — a real client built with a mock `fetch` whose body is a `ReadableStream` of\n * newline-delimited JSON. This exercises the `ollama` lib's actual JSON-lines stream parsing\n * flowing through `pullModel`'s loop — the framing that layer 1 stubs over.\n */\nfunction ndjsonResponse(lines: ReadonlyArray<string>): Response {\n const bytes = new TextEncoder().encode(lines.join('\\n') + '\\n');\n // Split the payload mid-stream (likely mid-line) into two reads to exercise buffer reassembly.\n const mid = Math.floor(bytes.length / 2);\n const parts = [bytes.slice(0, mid), bytes.slice(mid)];\n let i = 0;\n const stream = new ReadableStream<Uint8Array>({\n pull(controller: ReadableStreamDefaultController<Uint8Array>): void {\n if (i < parts.length) {\n controller.enqueue(parts[i++]);\n } else {\n controller.close();\n }\n }\n });\n return new Response(stream, { status: 200 });\n}\n\ndescribe('pullModel (layer 2 — fetch-level JSON-lines integration)', () => {\n test('parses a real newline-delimited /api/pull stream through the ollama lib', async () => {\n const lines = [\n JSON.stringify({ status: 'pulling manifest' }),\n JSON.stringify({ status: 'pulling sha256:abc', digest: 'sha256:abc', total: 1000, completed: 1000 }),\n JSON.stringify({ status: 'verifying sha256 digest' }),\n JSON.stringify({ status: 'writing manifest' }),\n JSON.stringify({ status: 'success' })\n ];\n const fetchMock = jest.fn().mockResolvedValue(ndjsonResponse(lines));\n const client = createOllamaClient({ fetch: fetchMock as unknown as typeof fetch }).orThrow();\n const seen: IOllamaPullProgress[] = [];\n\n expect(\n await pullModel(client, { model: 'llama3.1:8b', onProgress: (p) => seen.push(p) })\n ).toSucceedAndSatisfy((r) => {\n expect(r.finalStatus).toBe('success');\n expect(r.chunkCount).toBe(5);\n });\n expect(seen.map((s) => s.status)).toEqual([\n 'pulling manifest',\n 'pulling sha256:abc',\n 'verifying sha256 digest',\n 'writing manifest',\n 'success'\n ]);\n expect(seen[1]).toEqual({\n status: 'pulling sha256:abc',\n digest: 'sha256:abc',\n total: 1000,\n completed: 1000\n });\n expect(seen[0].digest).toBeUndefined();\n expect(fetchMock.mock.calls[0][0]).toEqual(expect.stringContaining('/api/pull'));\n });\n});\n"]}
@@ -0,0 +1,365 @@
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
+
32
+ import { JsonSchema } from '@fgv/ts-json-base';
33
+ import { JsonValue } from '@fgv/ts-json-base';
34
+ import { Ollama } from 'ollama';
35
+ import { Result } from '@fgv/ts-utils';
36
+
37
+ /**
38
+ * Single-turn grammar-constrained structured chat (`POST /api/chat` with `format` = full JSON
39
+ * schema). The fidelity win over ai-assist's prompt-and-parse path is **grammar-constrained
40
+ * sampling**: Ollama restricts the token sampler to the supplied schema, so the response is
41
+ * structurally guaranteed to match.
42
+ *
43
+ * The schema is the single source of truth: `schema.toJson()` (draft-07-sanitized) is sent as the
44
+ * wire `format`, and the same `schema.validate()` checks the reply — they cannot drift. `T` flows
45
+ * end-to-end from `JsonSchema.Static<typeof schema>`; no caller-supplied `T`, no cast.
46
+ *
47
+ * Runs over the streaming chat path internally, assembling the full document from the stream
48
+ * chunks and validating it whole — partial JSON has no validation value. (The design §4 sketch
49
+ * said `stream: false`, but the locked OQ-3 amendment requires an `AbortSignal`, and the `ollama`
50
+ * lib only threads a signal on the streaming path — its non-streaming request has no signal
51
+ * plumbing. The streaming `AbortableAsyncIterator.abort()` is "the mechanism that already exists
52
+ * for `pullModel`".) Any upstream error, abort, parse failure, or validation failure surfaces as a
53
+ * single `Failure` with context.
54
+ *
55
+ * @param client - A client from {@link createOllamaClient}.
56
+ * @param params - {@link IChatStructuredParams} — model, messages, the `JsonSchema` schema, and
57
+ * optional `options` / `keepAlive` / `signal`.
58
+ * @returns `Promise<Result<IOllamaChatStructuredResult<T>>>`.
59
+ * @public
60
+ */
61
+ export declare function chatStructured<T>(client: IOllamaClient, params: IChatStructuredParams<T>): Promise<Result<IOllamaChatStructuredResult<T>>>;
62
+
63
+ /**
64
+ * Constructs an Ollama client. Synchronous (the constructor performs no I/O), so returns
65
+ * `Result<IOllamaClient>` rather than a promise. A malformed `host` (which the upstream
66
+ * constructor rejects) surfaces as `Result.fail`.
67
+ *
68
+ * @param params - Optional {@link ICreateOllamaClientParams}. Omitted fields fall back to the
69
+ * `ollama` library defaults.
70
+ * @returns `Result<IOllamaClient>` wrapping the upstream `Ollama` instance.
71
+ * @public
72
+ */
73
+ export declare function createOllamaClient(params?: ICreateOllamaClientParams): Result<IOllamaClient>;
74
+
75
+ /**
76
+ * Deletes a model from the local store (`DELETE /api/delete`). Returns a meaningful
77
+ * {@link IOllamaDeleteResult} rather than `Result<void>`.
78
+ *
79
+ * @param client - A client from {@link createOllamaClient}.
80
+ * @param model - The model name to delete.
81
+ * @returns `Promise<Result<IOllamaDeleteResult>>`; a missing model or transport error surfaces as
82
+ * `Failure`.
83
+ * @public
84
+ */
85
+ export declare function deleteModel(client: IOllamaClient, model: string): Promise<Result<IOllamaDeleteResult>>;
86
+
87
+ /**
88
+ * Parameters for {@link chatStructured}.
89
+ * @public
90
+ */
91
+ export declare interface IChatStructuredParams<T> {
92
+ /** The model to chat with, e.g. `'llama3.1:8b'`. */
93
+ readonly model: string;
94
+ /** The conversation so far. */
95
+ readonly messages: ReadonlyArray<IOllamaChatMessage>;
96
+ /**
97
+ * The single source of truth for both the wire schema (`schema.toJson()`, sent as the native
98
+ * `format` field) and the result validator (`schema.validate()`). Author with
99
+ * `JsonSchema.object(...)` from `@fgv/ts-json-base`; `T` is derived via
100
+ * `JsonSchema.Static<typeof schema>`. A runtime-discovered (MCP) schema parsed via
101
+ * `JsonSchema.fromJson(raw)` also works — its `T` is `JsonValue`.
102
+ */
103
+ readonly schema: JsonSchema.ISchemaValidator<T>;
104
+ /** Native Ollama `options` (temperature, num_ctx, seed, …). Passed verbatim. */
105
+ readonly options?: Readonly<Record<string, JsonValue>>;
106
+ /** Native `keep_alive` (duration string or seconds). Passed verbatim — no policy applied. */
107
+ readonly keepAlive?: string | number;
108
+ /**
109
+ * Abort signal. Cancels the in-flight generation; the upstream stream throws an `AbortError`,
110
+ * which surfaces as `Result.fail`. An already-aborted signal cancels immediately.
111
+ */
112
+ readonly signal?: AbortSignal;
113
+ }
114
+
115
+ /**
116
+ * Construction parameters for an Ollama client. All optional; defaults match the `ollama` JS
117
+ * library (host `http://127.0.0.1:11434`).
118
+ * @public
119
+ */
120
+ export declare interface ICreateOllamaClientParams {
121
+ /** Server address. Defaults to `http://127.0.0.1:11434`. */
122
+ readonly host?: string;
123
+ /**
124
+ * Custom `fetch` implementation. Injecting a fetch is the primary unit-test seam and the hook
125
+ * for a future proxy / browser path.
126
+ */
127
+ readonly fetch?: typeof fetch;
128
+ /** Custom headers included on every request (e.g. an auth token for a guarded sidecar). */
129
+ readonly headers?: Record<string, string>;
130
+ }
131
+
132
+ /**
133
+ * A chat message in Ollama native shape.
134
+ * @public
135
+ */
136
+ export declare interface IOllamaChatMessage {
137
+ /** The message role. */
138
+ readonly role: 'system' | 'user' | 'assistant' | 'tool';
139
+ /** The message text. */
140
+ readonly content: string;
141
+ /** Base64 image data for vision models (no `data:` prefix). */
142
+ readonly images?: ReadonlyArray<string>;
143
+ }
144
+
145
+ /**
146
+ * Result of a grammar-constrained structured chat. Carries both the validated typed value and the
147
+ * raw JSON text the model emitted, for diagnostics.
148
+ * @public
149
+ */
150
+ export declare interface IOllamaChatStructuredResult<T> {
151
+ /** The parsed-and-validated value (validated by the same schema sent on the wire). */
152
+ readonly value: T;
153
+ /** The raw `message.content` JSON string the model emitted (assembled across stream chunks). */
154
+ readonly raw: string;
155
+ /** The model that produced the response. */
156
+ readonly model: string;
157
+ /** Provider-reported finish reason, when present. */
158
+ readonly doneReason?: string;
159
+ }
160
+
161
+ /**
162
+ * Opaque client handle. Re-export of the upstream `ollama` `Ollama` instance type — consumers
163
+ * retain full access to the upstream API for anything this boundary does not wrap.
164
+ * @public
165
+ */
166
+ export declare type IOllamaClient = Ollama;
167
+
168
+ /**
169
+ * Result of a delete. Returns a meaningful value rather than `Result<void>` (per repo standards —
170
+ * `Result<void>` is an anti-pattern).
171
+ * @public
172
+ */
173
+ export declare interface IOllamaDeleteResult {
174
+ /** The model that was deleted. */
175
+ readonly model: string;
176
+ /** Always `true` — a failed delete surfaces as `Result.fail`, never `deleted: false`. */
177
+ readonly deleted: true;
178
+ }
179
+
180
+ /**
181
+ * A model present in the local store (`/api/tags`).
182
+ * @public
183
+ */
184
+ export declare interface IOllamaModel extends IOllamaModelBase {
185
+ /** Last-modified timestamp, parsed from the RFC3339 wire value. */
186
+ readonly modifiedAt: Date;
187
+ }
188
+
189
+ /**
190
+ * Fields common to a locally-stored model and a running model.
191
+ * @public
192
+ */
193
+ export declare interface IOllamaModelBase {
194
+ /** Tagged name, e.g. `'llama3.1:8b'`. */
195
+ readonly name: string;
196
+ /** Underlying model id (often equal to `name`). */
197
+ readonly model: string;
198
+ /** On-disk size in bytes. */
199
+ readonly size: number;
200
+ /** SHA-256 manifest digest. */
201
+ readonly digest: string;
202
+ /** GGUF / architecture detail. */
203
+ readonly details: IOllamaModelDetail;
204
+ }
205
+
206
+ /**
207
+ * GGUF / model detail block, common to `/api/tags`, `/api/ps`, and `/api/show`. All fields
208
+ * optional — Ollama omits any it cannot determine.
209
+ * @public
210
+ */
211
+ export declare interface IOllamaModelDetail {
212
+ /** Storage format, e.g. `'gguf'`. */
213
+ readonly format?: string;
214
+ /** Primary architecture family, e.g. `'llama'`. */
215
+ readonly family?: string;
216
+ /** All architecture families this model belongs to. */
217
+ readonly families?: ReadonlyArray<string>;
218
+ /** Human-readable parameter count, e.g. `'8.0B'`. */
219
+ readonly parameterSize?: string;
220
+ /** Quantization level, e.g. `'Q4_0'`. */
221
+ readonly quantizationLevel?: string;
222
+ /** Parent model for fine-tunes / derived models. */
223
+ readonly parentModel?: string;
224
+ }
225
+
226
+ /**
227
+ * Full model detail, capabilities, parameters, and template (`POST /api/show`).
228
+ * @public
229
+ */
230
+ export declare interface IOllamaModelInfo {
231
+ /** The Modelfile source, when returned. */
232
+ readonly modelfile?: string;
233
+ /** Default parameters string. */
234
+ readonly parameters?: string;
235
+ /** The prompt template. */
236
+ readonly template?: string;
237
+ /** GGUF / architecture detail. */
238
+ readonly details: IOllamaModelDetail;
239
+ /** Low-level model metadata key-value map (`model_info`). */
240
+ readonly modelInfo?: Readonly<Record<string, JsonValue>>;
241
+ /** Declared capabilities, e.g. `['completion', 'tools', 'vision', 'thinking']`. */
242
+ readonly capabilities?: ReadonlyArray<string>;
243
+ }
244
+
245
+ /**
246
+ * A single progress chunk from a streamed pull (`/api/pull`). Ollama emits a JSON-lines stream;
247
+ * each line is one of these. Fields map 1:1 from the upstream `ProgressResponse`; the layer fields
248
+ * (`digest`/`total`/`completed`) are `undefined` on the manifest / verify / write phases.
249
+ * @public
250
+ */
251
+ export declare interface IOllamaPullProgress {
252
+ /**
253
+ * Phase string, e.g. `'pulling manifest'`, `'pulling <digest>'`, `'verifying sha256 digest'`,
254
+ * `'writing manifest'`, `'success'`.
255
+ */
256
+ readonly status: string;
257
+ /** Layer digest being transferred (present during `'pulling <digest>'` phases). */
258
+ readonly digest?: string;
259
+ /** Total bytes for the current layer, when known. */
260
+ readonly total?: number;
261
+ /** Bytes transferred so far for the current layer, when known. */
262
+ readonly completed?: number;
263
+ }
264
+
265
+ /**
266
+ * Terminal result of a completed pull. Returns a meaningful value (not `Result<void>`): the final
267
+ * status plus a count of progress chunks observed.
268
+ * @public
269
+ */
270
+ export declare interface IOllamaPullResult {
271
+ /** The model that was pulled. */
272
+ readonly model: string;
273
+ /** The `status` of the final chunk, normally `'success'`. */
274
+ readonly finalStatus: string;
275
+ /** Number of progress chunks observed (useful for tests and diagnostics). */
276
+ readonly chunkCount: number;
277
+ }
278
+
279
+ /**
280
+ * A model currently loaded into memory (`/api/ps`). Note `/api/ps` does NOT return
281
+ * `modified_at`, so this does not extend {@link IOllamaModel}.
282
+ * @public
283
+ */
284
+ export declare interface IOllamaRunningModel extends IOllamaModelBase {
285
+ /** When the model will be unloaded, parsed from the RFC3339 wire value. */
286
+ readonly expiresAt: Date;
287
+ /** Bytes resident in VRAM (0 when fully on CPU). */
288
+ readonly sizeVram: number;
289
+ }
290
+
291
+ /**
292
+ * Parameters for {@link pullModel}.
293
+ * @public
294
+ */
295
+ export declare interface IPullModelParams {
296
+ /** Model to pull, e.g. `'llama3.1:8b'`. */
297
+ readonly model: string;
298
+ /** Allow pulling from insecure (non-TLS) registries. */
299
+ readonly insecure?: boolean;
300
+ /**
301
+ * Progress callback, invoked once per JSON-lines chunk as it arrives. The `Result` returned by
302
+ * {@link pullModel} resolves only after the stream terminates; progress is surfaced here in the
303
+ * interim. A throw from this callback fails the whole pull (it runs inside the captured loop).
304
+ */
305
+ readonly onProgress?: (progress: IOllamaPullProgress) => void;
306
+ /**
307
+ * Abort signal. Cancels the in-flight pull; the upstream stream throws an `AbortError`, which
308
+ * surfaces as `Result.fail`. An already-aborted signal cancels immediately.
309
+ */
310
+ readonly signal?: AbortSignal;
311
+ }
312
+
313
+ /**
314
+ * Lists models in the local store (`GET /api/tags`). Each entry is normalized to
315
+ * {@link IOllamaModel} (camelCase, `modifiedAt` as a `Date`).
316
+ *
317
+ * @param client - A client from {@link createOllamaClient}.
318
+ * @returns `Promise<Result<ReadonlyArray<IOllamaModel>>>`; upstream / transport errors are captured
319
+ * as `Failure`.
320
+ * @public
321
+ */
322
+ export declare function listModels(client: IOllamaClient): Promise<Result<ReadonlyArray<IOllamaModel>>>;
323
+
324
+ /**
325
+ * Lists models currently loaded into memory (`GET /api/ps`). Each entry is normalized to
326
+ * {@link IOllamaRunningModel} (camelCase, `expiresAt` as a `Date`, `sizeVram` in bytes).
327
+ *
328
+ * @param client - A client from {@link createOllamaClient}.
329
+ * @returns `Promise<Result<ReadonlyArray<IOllamaRunningModel>>>`; upstream / transport errors are
330
+ * captured as `Failure`.
331
+ * @public
332
+ */
333
+ export declare function listRunning(client: IOllamaClient): Promise<Result<ReadonlyArray<IOllamaRunningModel>>>;
334
+
335
+ /**
336
+ * Pulls a model with streamed progress (`POST /api/pull`). Drives the upstream JSON-lines stream
337
+ * internally, invoking `params.onProgress` once per chunk, and resolves the `Result` once the
338
+ * stream terminates. Any upstream error — connection drop, registry 404, abort, or a throwing
339
+ * `onProgress` — becomes a single `Failure`; success returns a meaningful {@link IOllamaPullResult}
340
+ * rather than `Result<void>`.
341
+ *
342
+ * @param client - A client from {@link createOllamaClient}.
343
+ * @param params - {@link IPullModelParams} — the model, optional `insecure` flag, optional
344
+ * `onProgress` callback, and optional `AbortSignal`.
345
+ * @returns `Promise<Result<IOllamaPullResult>>`.
346
+ * @public
347
+ */
348
+ export declare function pullModel(client: IOllamaClient, params: IPullModelParams): Promise<Result<IOllamaPullResult>>;
349
+
350
+ /**
351
+ * Full model detail, capabilities, parameters, and template (`POST /api/show`), normalized to
352
+ * {@link IOllamaModelInfo}.
353
+ *
354
+ * @param client - A client from {@link createOllamaClient}.
355
+ * @param model - The model name to inspect, e.g. `'llama3.1:8b'`.
356
+ * @param options - Optional flags. `verbose: true` requests the full low-level `model_info` block.
357
+ * @returns `Promise<Result<IOllamaModelInfo>>`; upstream / transport errors (e.g. unknown model)
358
+ * are captured as `Failure`.
359
+ * @public
360
+ */
361
+ export declare function showModel(client: IOllamaClient, model: string, options?: {
362
+ readonly verbose?: boolean;
363
+ }): Promise<Result<IOllamaModelInfo>>;
364
+
365
+ export { }
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.58.7"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,15 @@
1
+ // ESLint 9 flat config
2
+ const nodeProfile = require('@rushstack/eslint-config/flat/profile/node');
3
+ const packletsPlugin = require('@rushstack/eslint-config/flat/mixins/packlets');
4
+ const tsdocPlugin = require('@rushstack/eslint-config/flat/mixins/tsdoc');
5
+
6
+ module.exports = [
7
+ ...nodeProfile,
8
+ packletsPlugin,
9
+ ...tsdocPlugin,
10
+ {
11
+ rules: {
12
+ '@rushstack/packlets/mechanics': 'warn'
13
+ }
14
+ }
15
+ ];