@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.
- package/.rush/temp/b70e5f6b6ada97ea70c5583027ea2b58b27bef46.tar.log +54 -0
- package/.rush/temp/chunked-rush-logs/ts-extras-ollama.build.chunks.jsonl +9 -0
- package/.rush/temp/operation/build/all.log +9 -0
- package/.rush/temp/operation/build/log-chunks.jsonl +9 -0
- package/.rush/temp/operation/build/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +681 -0
- package/CHANGELOG.json +4 -0
- package/README.md +105 -0
- package/config/api-extractor.json +38 -0
- package/config/jest.config.json +13 -0
- package/config/rig.json +6 -0
- package/dist/index.js +388 -0
- package/dist/index.js.map +1 -0
- package/dist/test/unit/chatStructured.test.js +287 -0
- package/dist/test/unit/chatStructured.test.js.map +1 -0
- package/dist/test/unit/fixtures/wireFixtures.js +90 -0
- package/dist/test/unit/fixtures/wireFixtures.js.map +1 -0
- package/dist/test/unit/modelManagement.test.js +118 -0
- package/dist/test/unit/modelManagement.test.js.map +1 -0
- package/dist/test/unit/ollamaClient.test.js +38 -0
- package/dist/test/unit/ollamaClient.test.js.map +1 -0
- package/dist/test/unit/pullModel.test.js +202 -0
- package/dist/test/unit/pullModel.test.js.map +1 -0
- package/dist/ts-extras-ollama.d.ts +365 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/eslint.config.js +15 -0
- package/etc/ts-extras-ollama.api.md +139 -0
- package/lib/index.d.ts +341 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +397 -0
- package/lib/index.js.map +1 -0
- package/lib/test/unit/chatStructured.test.d.ts +2 -0
- package/lib/test/unit/chatStructured.test.d.ts.map +1 -0
- package/lib/test/unit/chatStructured.test.js +289 -0
- package/lib/test/unit/chatStructured.test.js.map +1 -0
- package/lib/test/unit/fixtures/wireFixtures.d.ts +19 -0
- package/lib/test/unit/fixtures/wireFixtures.d.ts.map +1 -0
- package/lib/test/unit/fixtures/wireFixtures.js +93 -0
- package/lib/test/unit/fixtures/wireFixtures.js.map +1 -0
- package/lib/test/unit/modelManagement.test.d.ts +2 -0
- package/lib/test/unit/modelManagement.test.d.ts.map +1 -0
- package/lib/test/unit/modelManagement.test.js +120 -0
- package/lib/test/unit/modelManagement.test.js.map +1 -0
- package/lib/test/unit/ollamaClient.test.d.ts +2 -0
- package/lib/test/unit/ollamaClient.test.d.ts.map +1 -0
- package/lib/test/unit/ollamaClient.test.js +40 -0
- package/lib/test/unit/ollamaClient.test.js.map +1 -0
- package/lib/test/unit/pullModel.test.d.ts +2 -0
- package/lib/test/unit/pullModel.test.d.ts.map +1 -0
- package/lib/test/unit/pullModel.test.js +204 -0
- package/lib/test/unit/pullModel.test.js.map +1 -0
- package/package.json +83 -0
- package/rush-logs/ts-extras-ollama.build.cache.log +3 -0
- package/rush-logs/ts-extras-ollama.build.log +9 -0
- package/src/index.ts +655 -0
- package/src/test/unit/chatStructured.test.ts +330 -0
- package/src/test/unit/fixtures/wireFixtures.ts +95 -0
- package/src/test/unit/modelManagement.test.ts +128 -0
- package/src/test/unit/ollamaClient.test.ts +44 -0
- package/src/test/unit/pullModel.test.ts +220 -0
- package/temp/build/lint/_eslint-5eVG3S6w.json +30 -0
- package/temp/build/typescript/ts_8nwakTlr.json +1 -0
- package/temp/ts-extras-ollama.api.json +2528 -0
- package/temp/ts-extras-ollama.api.md +139 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollamaClient.test.d.ts","sourceRoot":"","sources":["../../../src/test/unit/ollamaClient.test.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
require("@fgv/ts-utils-jest");
|
|
4
|
+
const ollama_1 = require("ollama");
|
|
5
|
+
const index_1 = require("../../index");
|
|
6
|
+
/**
|
|
7
|
+
* Reads the upstream client's (protected) resolved host for assertion. The `Ollama` instance
|
|
8
|
+
* keeps its config on a protected field; tests reach it through a narrow cast rather than widening
|
|
9
|
+
* the production surface.
|
|
10
|
+
*/
|
|
11
|
+
function resolvedHost(client) {
|
|
12
|
+
return client.config.host;
|
|
13
|
+
}
|
|
14
|
+
describe('createOllamaClient', () => {
|
|
15
|
+
test('succeeds with default host when no params are supplied', () => {
|
|
16
|
+
expect((0, index_1.createOllamaClient)()).toSucceedAndSatisfy((client) => {
|
|
17
|
+
expect(client).toBeInstanceOf(ollama_1.Ollama);
|
|
18
|
+
expect(resolvedHost(client)).toBe('http://127.0.0.1:11434');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
test('applies a custom host', () => {
|
|
22
|
+
expect((0, index_1.createOllamaClient)({ host: 'http://10.0.0.5:11434' })).toSucceedAndSatisfy((client) => {
|
|
23
|
+
expect(resolvedHost(client)).toBe('http://10.0.0.5:11434');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
test('accepts a custom fetch and headers', () => {
|
|
27
|
+
const customFetch = jest.fn();
|
|
28
|
+
expect((0, index_1.createOllamaClient)({
|
|
29
|
+
host: 'http://localhost:11434',
|
|
30
|
+
fetch: customFetch,
|
|
31
|
+
headers: { Authorization: 'Bearer token' }
|
|
32
|
+
})).toSucceedAndSatisfy((client) => {
|
|
33
|
+
expect(client).toBeInstanceOf(ollama_1.Ollama);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
test('fails when the host is malformed (upstream constructor throws)', () => {
|
|
37
|
+
expect((0, index_1.createOllamaClient)({ host: 'not a url' })).toFailWith(/invalid url/i);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=ollamaClient.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollamaClient.test.js","sourceRoot":"","sources":["../../../src/test/unit/ollamaClient.test.ts"],"names":[],"mappings":";;AAAA,8BAA4B;AAC5B,mCAAgC;AAChC,uCAAiD;AAEjD;;;;GAIG;AACH,SAAS,YAAY,CAAC,MAAe;IACnC,OAAQ,MAAuC,CAAC,MAAM,CAAC,IAAI,CAAC;AAC9D,CAAC;AAED,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,IAAI,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAClE,MAAM,CAAC,IAAA,0BAAkB,GAAE,CAAC,CAAC,mBAAmB,CAAC,CAAC,MAAM,EAAE,EAAE;YAC1D,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,eAAM,CAAC,CAAC;YACtC,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,IAAA,0BAAkB,EAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,MAAM,EAAE,EAAE;YAC3F,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC9C,MAAM,WAAW,GAAG,IAAI,CAAC,EAAE,EAA6B,CAAC;QACzD,MAAM,CACJ,IAAA,0BAAkB,EAAC;YACjB,IAAI,EAAE,wBAAwB;YAC9B,KAAK,EAAE,WAAW;YAClB,OAAO,EAAE,EAAE,aAAa,EAAE,cAAc,EAAE;SAC3C,CAAC,CACH,CAAC,mBAAmB,CAAC,CAAC,MAAM,EAAE,EAAE;YAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,eAAM,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,gEAAgE,EAAE,GAAG,EAAE;QAC1E,MAAM,CAAC,IAAA,0BAAkB,EAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import '@fgv/ts-utils-jest';\nimport { Ollama } from 'ollama';\nimport { createOllamaClient } from '../../index';\n\n/**\n * Reads the upstream client's (protected) resolved host for assertion. The `Ollama` instance\n * keeps its config on a protected field; tests reach it through a narrow cast rather than widening\n * the production surface.\n */\nfunction resolvedHost(client: unknown): string {\n return (client as { config: { host: string } }).config.host;\n}\n\ndescribe('createOllamaClient', () => {\n test('succeeds with default host when no params are supplied', () => {\n expect(createOllamaClient()).toSucceedAndSatisfy((client) => {\n expect(client).toBeInstanceOf(Ollama);\n expect(resolvedHost(client)).toBe('http://127.0.0.1:11434');\n });\n });\n\n test('applies a custom host', () => {\n expect(createOllamaClient({ host: 'http://10.0.0.5:11434' })).toSucceedAndSatisfy((client) => {\n expect(resolvedHost(client)).toBe('http://10.0.0.5:11434');\n });\n });\n\n test('accepts a custom fetch and headers', () => {\n const customFetch = jest.fn() as unknown as typeof fetch;\n expect(\n createOllamaClient({\n host: 'http://localhost:11434',\n fetch: customFetch,\n headers: { Authorization: 'Bearer token' }\n })\n ).toSucceedAndSatisfy((client) => {\n expect(client).toBeInstanceOf(Ollama);\n });\n });\n\n test('fails when the host is malformed (upstream constructor throws)', () => {\n expect(createOllamaClient({ host: 'not a url' })).toFailWith(/invalid url/i);\n });\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pullModel.test.d.ts","sourceRoot":"","sources":["../../../src/test/unit/pullModel.test.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
|
|
3
|
+
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
|
|
4
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
5
|
+
var g = generator.apply(thisArg, _arguments || []), i, q = [];
|
|
6
|
+
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
|
|
7
|
+
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
|
|
8
|
+
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]); } }
|
|
9
|
+
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
|
|
10
|
+
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
|
|
11
|
+
function fulfill(value) { resume("next", value); }
|
|
12
|
+
function reject(value) { resume("throw", value); }
|
|
13
|
+
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
require("@fgv/ts-utils-jest");
|
|
17
|
+
const index_1 = require("../../index");
|
|
18
|
+
/**
|
|
19
|
+
* Builds a structural mock of the upstream `AbortableAsyncIterator` returned by `client.pull`.
|
|
20
|
+
* `abort()` flips an internal flag that the generator checks before each chunk — so an abort
|
|
21
|
+
* triggered mid-stream surfaces as a throw on the next iteration, matching the real lib.
|
|
22
|
+
*/
|
|
23
|
+
function makePullIterator(chunks, opts) {
|
|
24
|
+
let aborted = false;
|
|
25
|
+
return {
|
|
26
|
+
abort: jest.fn(() => {
|
|
27
|
+
aborted = true;
|
|
28
|
+
}),
|
|
29
|
+
[Symbol.asyncIterator]() {
|
|
30
|
+
return __asyncGenerator(this, arguments, function* _a() {
|
|
31
|
+
for (const chunk of chunks) {
|
|
32
|
+
if (aborted) {
|
|
33
|
+
throw new Error('The operation was aborted');
|
|
34
|
+
}
|
|
35
|
+
yield yield __await(chunk);
|
|
36
|
+
if (opts === null || opts === void 0 ? void 0 : opts.throwMidStream) {
|
|
37
|
+
throw new Error('stream broke mid-pull');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function mockPullClient(pull) {
|
|
45
|
+
return { pull };
|
|
46
|
+
}
|
|
47
|
+
const happyChunks = [
|
|
48
|
+
{ status: 'pulling manifest' },
|
|
49
|
+
{ status: 'pulling sha256:abc', digest: 'sha256:abc', total: 1000, completed: 1000 },
|
|
50
|
+
{ status: 'success' }
|
|
51
|
+
];
|
|
52
|
+
describe('pullModel (layer 1 — structural client mock)', () => {
|
|
53
|
+
test('drives the stream, maps each chunk, and resolves with finalStatus + chunkCount', async () => {
|
|
54
|
+
const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));
|
|
55
|
+
const client = mockPullClient(pull);
|
|
56
|
+
const seen = [];
|
|
57
|
+
expect(await (0, index_1.pullModel)(client, { model: 'llama3.1:8b', onProgress: (p) => seen.push(p) })).toSucceedWith({
|
|
58
|
+
model: 'llama3.1:8b',
|
|
59
|
+
finalStatus: 'success',
|
|
60
|
+
chunkCount: 3
|
|
61
|
+
});
|
|
62
|
+
expect(pull).toHaveBeenCalledWith({ model: 'llama3.1:8b', stream: true });
|
|
63
|
+
expect(seen).toHaveLength(3);
|
|
64
|
+
// Manifest phase: no layer fields.
|
|
65
|
+
expect(seen[0]).toEqual({
|
|
66
|
+
status: 'pulling manifest',
|
|
67
|
+
digest: undefined,
|
|
68
|
+
total: undefined,
|
|
69
|
+
completed: undefined
|
|
70
|
+
});
|
|
71
|
+
// Transfer phase: layer fields present and mapped 1:1.
|
|
72
|
+
expect(seen[1]).toEqual({
|
|
73
|
+
status: 'pulling sha256:abc',
|
|
74
|
+
digest: 'sha256:abc',
|
|
75
|
+
total: 1000,
|
|
76
|
+
completed: 1000
|
|
77
|
+
});
|
|
78
|
+
expect(seen[2].status).toBe('success');
|
|
79
|
+
});
|
|
80
|
+
test('forwards insecure and works without an onProgress callback', async () => {
|
|
81
|
+
const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));
|
|
82
|
+
const client = mockPullClient(pull);
|
|
83
|
+
expect(await (0, index_1.pullModel)(client, { model: 'llama3.1:8b', insecure: true })).toSucceedWith({
|
|
84
|
+
model: 'llama3.1:8b',
|
|
85
|
+
finalStatus: 'success',
|
|
86
|
+
chunkCount: 3
|
|
87
|
+
});
|
|
88
|
+
expect(pull).toHaveBeenCalledWith({ model: 'llama3.1:8b', stream: true, insecure: true });
|
|
89
|
+
});
|
|
90
|
+
test('fails when the stream errors mid-pull', async () => {
|
|
91
|
+
const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks, { throwMidStream: true }));
|
|
92
|
+
const client = mockPullClient(pull);
|
|
93
|
+
expect(await (0, index_1.pullModel)(client, { model: 'llama3.1:8b' })).toFailWith(/stream broke mid-pull/);
|
|
94
|
+
});
|
|
95
|
+
test('fails when onProgress throws', async () => {
|
|
96
|
+
const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));
|
|
97
|
+
const client = mockPullClient(pull);
|
|
98
|
+
expect(await (0, index_1.pullModel)(client, {
|
|
99
|
+
model: 'llama3.1:8b',
|
|
100
|
+
onProgress: () => {
|
|
101
|
+
throw new Error('consumer blew up');
|
|
102
|
+
}
|
|
103
|
+
})).toFailWith(/consumer blew up/);
|
|
104
|
+
});
|
|
105
|
+
test('an already-aborted signal cancels the pull on the first iteration', async () => {
|
|
106
|
+
// The signal is aborted before the loop begins, so the iterator's abort fires up front; the
|
|
107
|
+
// throw surfaces on the first iteration attempt (the mock checks the flag before the first yield).
|
|
108
|
+
const iterator = makePullIterator(happyChunks);
|
|
109
|
+
const pull = jest.fn().mockResolvedValue(iterator);
|
|
110
|
+
const client = mockPullClient(pull);
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
controller.abort();
|
|
113
|
+
expect(await (0, index_1.pullModel)(client, { model: 'llama3.1:8b', signal: controller.signal })).toFailWith(/abort/i);
|
|
114
|
+
expect(iterator.abort).toHaveBeenCalledTimes(1);
|
|
115
|
+
});
|
|
116
|
+
test('a mid-stream abort cancels the pull, removes the listener, and surfaces as failure', async () => {
|
|
117
|
+
const iterator = makePullIterator(happyChunks);
|
|
118
|
+
const pull = jest.fn().mockResolvedValue(iterator);
|
|
119
|
+
const client = mockPullClient(pull);
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const removeSpy = jest.spyOn(controller.signal, 'removeEventListener');
|
|
122
|
+
const seen = [];
|
|
123
|
+
const result = await (0, index_1.pullModel)(client, {
|
|
124
|
+
model: 'llama3.1:8b',
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
onProgress: (p) => {
|
|
127
|
+
seen.push(p);
|
|
128
|
+
if (seen.length === 1) {
|
|
129
|
+
controller.abort();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
expect(result).toFailWith(/abort/i);
|
|
134
|
+
expect(iterator.abort).toHaveBeenCalled();
|
|
135
|
+
expect(seen).toHaveLength(1);
|
|
136
|
+
// The `finally` cleanup runs on the failure path too — no listener leak after an abort.
|
|
137
|
+
expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function));
|
|
138
|
+
});
|
|
139
|
+
test('removes the abort listener after a normal completion', async () => {
|
|
140
|
+
const pull = jest.fn().mockResolvedValue(makePullIterator(happyChunks));
|
|
141
|
+
const client = mockPullClient(pull);
|
|
142
|
+
const controller = new AbortController();
|
|
143
|
+
const removeSpy = jest.spyOn(controller.signal, 'removeEventListener');
|
|
144
|
+
expect(await (0, index_1.pullModel)(client, { model: 'llama3.1:8b', signal: controller.signal })).toSucceed();
|
|
145
|
+
expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function));
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* Layer 2 — a real client built with a mock `fetch` whose body is a `ReadableStream` of
|
|
150
|
+
* newline-delimited JSON. This exercises the `ollama` lib's actual JSON-lines stream parsing
|
|
151
|
+
* flowing through `pullModel`'s loop — the framing that layer 1 stubs over.
|
|
152
|
+
*/
|
|
153
|
+
function ndjsonResponse(lines) {
|
|
154
|
+
const bytes = new TextEncoder().encode(lines.join('\n') + '\n');
|
|
155
|
+
// Split the payload mid-stream (likely mid-line) into two reads to exercise buffer reassembly.
|
|
156
|
+
const mid = Math.floor(bytes.length / 2);
|
|
157
|
+
const parts = [bytes.slice(0, mid), bytes.slice(mid)];
|
|
158
|
+
let i = 0;
|
|
159
|
+
const stream = new ReadableStream({
|
|
160
|
+
pull(controller) {
|
|
161
|
+
if (i < parts.length) {
|
|
162
|
+
controller.enqueue(parts[i++]);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
controller.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return new Response(stream, { status: 200 });
|
|
170
|
+
}
|
|
171
|
+
describe('pullModel (layer 2 — fetch-level JSON-lines integration)', () => {
|
|
172
|
+
test('parses a real newline-delimited /api/pull stream through the ollama lib', async () => {
|
|
173
|
+
const lines = [
|
|
174
|
+
JSON.stringify({ status: 'pulling manifest' }),
|
|
175
|
+
JSON.stringify({ status: 'pulling sha256:abc', digest: 'sha256:abc', total: 1000, completed: 1000 }),
|
|
176
|
+
JSON.stringify({ status: 'verifying sha256 digest' }),
|
|
177
|
+
JSON.stringify({ status: 'writing manifest' }),
|
|
178
|
+
JSON.stringify({ status: 'success' })
|
|
179
|
+
];
|
|
180
|
+
const fetchMock = jest.fn().mockResolvedValue(ndjsonResponse(lines));
|
|
181
|
+
const client = (0, index_1.createOllamaClient)({ fetch: fetchMock }).orThrow();
|
|
182
|
+
const seen = [];
|
|
183
|
+
expect(await (0, index_1.pullModel)(client, { model: 'llama3.1:8b', onProgress: (p) => seen.push(p) })).toSucceedAndSatisfy((r) => {
|
|
184
|
+
expect(r.finalStatus).toBe('success');
|
|
185
|
+
expect(r.chunkCount).toBe(5);
|
|
186
|
+
});
|
|
187
|
+
expect(seen.map((s) => s.status)).toEqual([
|
|
188
|
+
'pulling manifest',
|
|
189
|
+
'pulling sha256:abc',
|
|
190
|
+
'verifying sha256 digest',
|
|
191
|
+
'writing manifest',
|
|
192
|
+
'success'
|
|
193
|
+
]);
|
|
194
|
+
expect(seen[1]).toEqual({
|
|
195
|
+
status: 'pulling sha256:abc',
|
|
196
|
+
digest: 'sha256:abc',
|
|
197
|
+
total: 1000,
|
|
198
|
+
completed: 1000
|
|
199
|
+
});
|
|
200
|
+
expect(seen[0].digest).toBeUndefined();
|
|
201
|
+
expect(fetchMock.mock.calls[0][0]).toEqual(expect.stringContaining('/api/pull'));
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
//# 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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fgv/ts-extras-ollama",
|
|
3
|
+
"version": "5.1.0-34",
|
|
4
|
+
"description": "Result-integration boundary over the ollama JS library for Node consumers (model management, streamed pull progress, grammar-constrained structured output)",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "dist/ts-extras-ollama.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"node": {
|
|
10
|
+
"import": "./lib/index.js",
|
|
11
|
+
"require": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"default": {
|
|
14
|
+
"import": "./lib/index.js",
|
|
15
|
+
"require": "./lib/index.js"
|
|
16
|
+
},
|
|
17
|
+
"types": "./dist/ts-extras-ollama.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "heft build --clean",
|
|
22
|
+
"clean": "heft clean",
|
|
23
|
+
"test": "heft test --clean",
|
|
24
|
+
"test-handles": "jest --runInBand --detectOpenHandles",
|
|
25
|
+
"clean-jest": "jest --clear-cache",
|
|
26
|
+
"update-snapshot": "jest --updateSnapshot",
|
|
27
|
+
"coverage": "jest --coverage",
|
|
28
|
+
"lint": "eslint src --ext .ts",
|
|
29
|
+
"fixlint": "eslint src --ext .ts --fix"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"typescript",
|
|
33
|
+
"ollama",
|
|
34
|
+
"result",
|
|
35
|
+
"local-ai",
|
|
36
|
+
"llm"
|
|
37
|
+
],
|
|
38
|
+
"author": "Erik Fortune",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/ErikFortune/fgv/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/ErikFortune/fgv/tree/main/libraries/ts-extras-ollama#readme",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/ErikFortune/fgv.git"
|
|
47
|
+
},
|
|
48
|
+
"sideEffects": false,
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@fgv/ts-json-base": "workspace:*"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@fgv/heft-dual-rig": "workspace:*",
|
|
54
|
+
"@fgv/ts-utils": "workspace:*",
|
|
55
|
+
"ollama": "^0.6.0",
|
|
56
|
+
"@fgv/ts-utils-jest": "workspace:*",
|
|
57
|
+
"@microsoft/api-extractor": "^7.55.2",
|
|
58
|
+
"@rushstack/eslint-config": "4.6.4",
|
|
59
|
+
"@rushstack/heft": "1.2.7",
|
|
60
|
+
"@rushstack/heft-jest-plugin": "1.2.6",
|
|
61
|
+
"@rushstack/heft-node-rig": "2.11.27",
|
|
62
|
+
"@types/heft-jest": "1.0.6",
|
|
63
|
+
"@types/jest": "^29.5.14",
|
|
64
|
+
"@types/node": "^20.14.9",
|
|
65
|
+
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
|
66
|
+
"@typescript-eslint/parser": "^8.52.0",
|
|
67
|
+
"eslint": "^9.39.2",
|
|
68
|
+
"eslint-plugin-import": "^2.32.0",
|
|
69
|
+
"eslint-plugin-n": "^17.23.1",
|
|
70
|
+
"eslint-plugin-node": "^11.1.0",
|
|
71
|
+
"eslint-plugin-promise": "^7.2.1",
|
|
72
|
+
"eslint-plugin-tsdoc": "~0.5.2",
|
|
73
|
+
"jest": "^29.7.0",
|
|
74
|
+
"rimraf": "^6.1.2",
|
|
75
|
+
"ts-jest": "^29.4.6",
|
|
76
|
+
"ts-node": "^10.9.2",
|
|
77
|
+
"typescript": "5.9.3"
|
|
78
|
+
},
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"@fgv/ts-utils": "workspace:*",
|
|
81
|
+
"ollama": "^0.6.0"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Invoking: heft build --clean
|
|
2
|
+
---- build started ----
|
|
3
|
+
[build:typescript] The TypeScript compiler version 5.9.3 is newer than the latest version that was tested with Heft (5.8); it may not work correctly.
|
|
4
|
+
[build:typescript] Using TypeScript version 5.9.3
|
|
5
|
+
[build:lint] Using ESLint version 9.39.4
|
|
6
|
+
[build:api-extractor] Using API Extractor version 7.58.7
|
|
7
|
+
[build:api-extractor] Analysis will use the bundled TypeScript version 5.9.3
|
|
8
|
+
---- build finished (1.887s) ----
|
|
9
|
+
-------------------- Finished (1.889s) --------------------
|