@nemesis-oss/ollama-sdk 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -970,8 +970,8 @@ declare class ModelsClient {
970
970
  * model-not-found (404), or VRAM-exhaustion failures.
971
971
  */
972
972
  pin(model: string): Promise<void>;
973
- createBlob(digest: string, data: BinaryBody): Promise<void>;
974
- checkBlob(digest: string): Promise<boolean>;
973
+ createBlob(digest: string, data: BinaryBody, request?: RequestCancellationOptions): Promise<void>;
974
+ checkBlob(digest: string, request?: RequestCancellationOptions): Promise<boolean>;
975
975
  /**
976
976
  * Computes a blob digest the way Ollama's content-addressed store expects:
977
977
  * SHA-256 over the raw bytes, formatted as `sha256:<64 lowercase hex>`.
@@ -992,17 +992,44 @@ declare class ModelsClient {
992
992
  */
993
993
  createBlobFromData(data: Uint8Array, opts?: {
994
994
  readonly digest?: string | undefined;
995
- readonly signal?: AbortSignal | undefined;
996
- }): Promise<BlobUploadResult>;
995
+ } & RequestCancellationOptions): Promise<BlobUploadResult>;
996
+ /**
997
+ * Streams a blob upload straight from a web `ReadableStream` — the
998
+ * multi-gigabyte-safe counterpart of {@link createBlobFromData}. The body
999
+ * is never materialized in memory: the transport hands the stream to
1000
+ * `fetch` directly (setting `duplex: 'half'`, which Node 18+ requires for
1001
+ * stream bodies), so peak heap usage is a transport-level chunk, not the
1002
+ * blob size.
1003
+ *
1004
+ * The digest must be known up front — it *is* the upload URL
1005
+ * (`/api/blobs/<digest>`), the same content-addressed protocol
1006
+ * `curl -T` performs. Hash first when you don't have it: in-memory bytes
1007
+ * via {@link computeBlobDigest}, files via {@link createBlobFromFile}
1008
+ * (which hashes in a streaming pass). The server verifies the streamed
1009
+ * bytes against the digest and rejects mismatches, so a corrupted or
1010
+ * truncated stream fails the upload instead of poisoning the store.
1011
+ */
1012
+ createBlobFromStream(digest: string, stream: ReadableStream<Uint8Array>, request?: RequestCancellationOptions): Promise<BlobUploadResult>;
997
1013
  /**
998
1014
  * Reads a local file and uploads it as a blob (Node.js only — dynamic
999
1015
  * `node:fs` import, so browser bundles never pull it in). The file name
1000
1016
  * (basename) is what `/api/create`'s `files` map expects as the key, so
1001
1017
  * this returns it alongside the digest.
1018
+ *
1019
+ * Streams both passes in fixed-size chunks — peak heap is O(chunk), not
1020
+ * O(file), so multi-gigabyte GGUFs never enter the Node heap:
1021
+ *
1022
+ * 1. **Hash pass** (skipped when `opts.digest` is provided): the file is
1023
+ * streamed through `node:crypto`'s incremental SHA-256.
1024
+ * 2. **Upload pass**: `node:fs` createReadStream → web ReadableStream →
1025
+ * `fetch` body with `duplex: 'half'` (see {@link createBlobFromStream}).
1026
+ *
1027
+ * A pre-existing blob (HEAD 200) skips the upload pass entirely, same as
1028
+ * {@link createBlobFromData}.
1002
1029
  */
1003
1030
  createBlobFromFile(path: string, opts?: {
1004
- readonly signal?: AbortSignal | undefined;
1005
- }): Promise<BlobUploadResult & {
1031
+ readonly digest?: string | undefined;
1032
+ } & RequestCancellationOptions): Promise<BlobUploadResult & {
1006
1033
  readonly fileName: string;
1007
1034
  }>;
1008
1035
  /**
@@ -2277,8 +2304,8 @@ declare class OllamaClient {
2277
2304
  */
2278
2305
  readonly getContextLength: (request: ContextDiscoveryRequestOptions) => Promise<DiscoveredContextLength>;
2279
2306
  readonly version: () => Promise<VersionResponse>;
2280
- readonly createBlob: (digest: string, data: BinaryBody) => Promise<void>;
2281
- readonly checkBlob: (digest: string) => Promise<boolean>;
2307
+ readonly createBlob: (digest: string, data: BinaryBody, request?: RequestCancellationOptions) => Promise<void>;
2308
+ readonly checkBlob: (digest: string, request?: RequestCancellationOptions) => Promise<boolean>;
2282
2309
  /**
2283
2310
  * Immediately unloads `model` from VRAM. Convenience alias for
2284
2311
  * {@link ModelsClient.unload} — see that method for the full
package/dist/index.d.ts CHANGED
@@ -970,8 +970,8 @@ declare class ModelsClient {
970
970
  * model-not-found (404), or VRAM-exhaustion failures.
971
971
  */
972
972
  pin(model: string): Promise<void>;
973
- createBlob(digest: string, data: BinaryBody): Promise<void>;
974
- checkBlob(digest: string): Promise<boolean>;
973
+ createBlob(digest: string, data: BinaryBody, request?: RequestCancellationOptions): Promise<void>;
974
+ checkBlob(digest: string, request?: RequestCancellationOptions): Promise<boolean>;
975
975
  /**
976
976
  * Computes a blob digest the way Ollama's content-addressed store expects:
977
977
  * SHA-256 over the raw bytes, formatted as `sha256:<64 lowercase hex>`.
@@ -992,17 +992,44 @@ declare class ModelsClient {
992
992
  */
993
993
  createBlobFromData(data: Uint8Array, opts?: {
994
994
  readonly digest?: string | undefined;
995
- readonly signal?: AbortSignal | undefined;
996
- }): Promise<BlobUploadResult>;
995
+ } & RequestCancellationOptions): Promise<BlobUploadResult>;
996
+ /**
997
+ * Streams a blob upload straight from a web `ReadableStream` — the
998
+ * multi-gigabyte-safe counterpart of {@link createBlobFromData}. The body
999
+ * is never materialized in memory: the transport hands the stream to
1000
+ * `fetch` directly (setting `duplex: 'half'`, which Node 18+ requires for
1001
+ * stream bodies), so peak heap usage is a transport-level chunk, not the
1002
+ * blob size.
1003
+ *
1004
+ * The digest must be known up front — it *is* the upload URL
1005
+ * (`/api/blobs/<digest>`), the same content-addressed protocol
1006
+ * `curl -T` performs. Hash first when you don't have it: in-memory bytes
1007
+ * via {@link computeBlobDigest}, files via {@link createBlobFromFile}
1008
+ * (which hashes in a streaming pass). The server verifies the streamed
1009
+ * bytes against the digest and rejects mismatches, so a corrupted or
1010
+ * truncated stream fails the upload instead of poisoning the store.
1011
+ */
1012
+ createBlobFromStream(digest: string, stream: ReadableStream<Uint8Array>, request?: RequestCancellationOptions): Promise<BlobUploadResult>;
997
1013
  /**
998
1014
  * Reads a local file and uploads it as a blob (Node.js only — dynamic
999
1015
  * `node:fs` import, so browser bundles never pull it in). The file name
1000
1016
  * (basename) is what `/api/create`'s `files` map expects as the key, so
1001
1017
  * this returns it alongside the digest.
1018
+ *
1019
+ * Streams both passes in fixed-size chunks — peak heap is O(chunk), not
1020
+ * O(file), so multi-gigabyte GGUFs never enter the Node heap:
1021
+ *
1022
+ * 1. **Hash pass** (skipped when `opts.digest` is provided): the file is
1023
+ * streamed through `node:crypto`'s incremental SHA-256.
1024
+ * 2. **Upload pass**: `node:fs` createReadStream → web ReadableStream →
1025
+ * `fetch` body with `duplex: 'half'` (see {@link createBlobFromStream}).
1026
+ *
1027
+ * A pre-existing blob (HEAD 200) skips the upload pass entirely, same as
1028
+ * {@link createBlobFromData}.
1002
1029
  */
1003
1030
  createBlobFromFile(path: string, opts?: {
1004
- readonly signal?: AbortSignal | undefined;
1005
- }): Promise<BlobUploadResult & {
1031
+ readonly digest?: string | undefined;
1032
+ } & RequestCancellationOptions): Promise<BlobUploadResult & {
1006
1033
  readonly fileName: string;
1007
1034
  }>;
1008
1035
  /**
@@ -2277,8 +2304,8 @@ declare class OllamaClient {
2277
2304
  */
2278
2305
  readonly getContextLength: (request: ContextDiscoveryRequestOptions) => Promise<DiscoveredContextLength>;
2279
2306
  readonly version: () => Promise<VersionResponse>;
2280
- readonly createBlob: (digest: string, data: BinaryBody) => Promise<void>;
2281
- readonly checkBlob: (digest: string) => Promise<boolean>;
2307
+ readonly createBlob: (digest: string, data: BinaryBody, request?: RequestCancellationOptions) => Promise<void>;
2308
+ readonly checkBlob: (digest: string, request?: RequestCancellationOptions) => Promise<boolean>;
2282
2309
  /**
2283
2310
  * Immediately unloads `model` from VRAM. Convenience alias for
2284
2311
  * {@link ModelsClient.unload} — see that method for the full
package/dist/index.js CHANGED
@@ -1111,6 +1111,18 @@ function createRequestId() {
1111
1111
  requestSequence += 1;
1112
1112
  return `ollama-http-${requestSequence}`;
1113
1113
  }
1114
+ function isWebReadableStreamBody(body) {
1115
+ return typeof body === "object" && body !== null && typeof body.getReader === "function";
1116
+ }
1117
+ function buildFetchInit(req) {
1118
+ return {
1119
+ method: req.method,
1120
+ headers: req.headers,
1121
+ ...req.body !== void 0 ? { body: req.body } : {},
1122
+ ...req.signal !== void 0 ? { signal: req.signal } : {},
1123
+ ...isWebReadableStreamBody(req.body) ? { duplex: "half" } : {}
1124
+ };
1125
+ }
1114
1126
  function sameHeaders(actual, expected) {
1115
1127
  const actualEntries = Object.fromEntries(
1116
1128
  Object.entries(actual).map(([key, value]) => [key.toLowerCase(), value])
@@ -1160,13 +1172,7 @@ var HttpClient = class {
1160
1172
  timestamp: startedAt
1161
1173
  });
1162
1174
  const finalHandler = async (req) => {
1163
- const init = {
1164
- method: req.method,
1165
- headers: req.headers,
1166
- ...req.body !== void 0 ? { body: req.body } : {},
1167
- ...req.signal !== void 0 ? { signal: req.signal } : {}
1168
- };
1169
- const response = await this.fetchImpl(req.url, init);
1175
+ const response = await this.fetchImpl(req.url, buildFetchInit(req));
1170
1176
  rawResponse = response;
1171
1177
  const responseHeaders = response.headers && typeof response.headers.entries === "function" ? Object.fromEntries(response.headers.entries()) : {};
1172
1178
  return {
@@ -1177,12 +1183,7 @@ var HttpClient = class {
1177
1183
  };
1178
1184
  try {
1179
1185
  if (this.middleware.length === 0) {
1180
- const response2 = await this.fetchImpl(request.url, {
1181
- method: request.method,
1182
- headers: request.headers,
1183
- ...request.body !== void 0 ? { body: request.body } : {},
1184
- ...request.signal !== void 0 ? { signal: request.signal } : {}
1185
- });
1186
+ const response2 = await this.fetchImpl(request.url, buildFetchInit(request));
1186
1187
  if (response2.status >= 200 && response2.status < 300) {
1187
1188
  this.onLifecycleEvent?.({
1188
1189
  type: "success",
@@ -2773,7 +2774,7 @@ var ModelsClient = class {
2773
2774
  { model, singleEndpoint: true }
2774
2775
  );
2775
2776
  }
2776
- async createBlob(digest, data) {
2777
+ async createBlob(digest, data, request) {
2777
2778
  await this.runner(
2778
2779
  (http, signal) => http.request({
2779
2780
  path: `/api/blobs/${encodeURIComponent(digest)}`,
@@ -2781,10 +2782,10 @@ var ModelsClient = class {
2781
2782
  rawBody: data,
2782
2783
  signal
2783
2784
  }),
2784
- { singleEndpoint: true }
2785
+ { ...request, singleEndpoint: true }
2785
2786
  );
2786
2787
  }
2787
- async checkBlob(digest) {
2788
+ async checkBlob(digest, request) {
2788
2789
  try {
2789
2790
  await this.runner(
2790
2791
  (http, signal) => http.request({
@@ -2792,7 +2793,7 @@ var ModelsClient = class {
2792
2793
  method: "HEAD",
2793
2794
  signal
2794
2795
  }),
2795
- { singleEndpoint: true }
2796
+ { ...request, singleEndpoint: true }
2796
2797
  );
2797
2798
  return true;
2798
2799
  } catch (err) {
@@ -2834,9 +2835,32 @@ var ModelsClient = class {
2834
2835
  */
2835
2836
  async createBlobFromData(data, opts) {
2836
2837
  const digest = opts?.digest ?? await this.computeBlobDigest(data);
2837
- const alreadyExisted = await this.checkBlob(digest);
2838
+ const alreadyExisted = await this.checkBlob(digest, opts);
2838
2839
  if (!alreadyExisted) {
2839
- await this.createBlob(digest, data);
2840
+ await this.createBlob(digest, data, opts);
2841
+ }
2842
+ return { digest, alreadyExisted };
2843
+ }
2844
+ /**
2845
+ * Streams a blob upload straight from a web `ReadableStream` — the
2846
+ * multi-gigabyte-safe counterpart of {@link createBlobFromData}. The body
2847
+ * is never materialized in memory: the transport hands the stream to
2848
+ * `fetch` directly (setting `duplex: 'half'`, which Node 18+ requires for
2849
+ * stream bodies), so peak heap usage is a transport-level chunk, not the
2850
+ * blob size.
2851
+ *
2852
+ * The digest must be known up front — it *is* the upload URL
2853
+ * (`/api/blobs/<digest>`), the same content-addressed protocol
2854
+ * `curl -T` performs. Hash first when you don't have it: in-memory bytes
2855
+ * via {@link computeBlobDigest}, files via {@link createBlobFromFile}
2856
+ * (which hashes in a streaming pass). The server verifies the streamed
2857
+ * bytes against the digest and rejects mismatches, so a corrupted or
2858
+ * truncated stream fails the upload instead of poisoning the store.
2859
+ */
2860
+ async createBlobFromStream(digest, stream, request) {
2861
+ const alreadyExisted = await this.checkBlob(digest, request);
2862
+ if (!alreadyExisted) {
2863
+ await this.createBlob(digest, stream, request);
2840
2864
  }
2841
2865
  return { digest, alreadyExisted };
2842
2866
  }
@@ -2845,16 +2869,61 @@ var ModelsClient = class {
2845
2869
  * `node:fs` import, so browser bundles never pull it in). The file name
2846
2870
  * (basename) is what `/api/create`'s `files` map expects as the key, so
2847
2871
  * this returns it alongside the digest.
2872
+ *
2873
+ * Streams both passes in fixed-size chunks — peak heap is O(chunk), not
2874
+ * O(file), so multi-gigabyte GGUFs never enter the Node heap:
2875
+ *
2876
+ * 1. **Hash pass** (skipped when `opts.digest` is provided): the file is
2877
+ * streamed through `node:crypto`'s incremental SHA-256.
2878
+ * 2. **Upload pass**: `node:fs` createReadStream → web ReadableStream →
2879
+ * `fetch` body with `duplex: 'half'` (see {@link createBlobFromStream}).
2880
+ *
2881
+ * A pre-existing blob (HEAD 200) skips the upload pass entirely, same as
2882
+ * {@link createBlobFromData}.
2848
2883
  */
2849
2884
  async createBlobFromFile(path, opts) {
2850
2885
  const nodeFsModule = "node:fs";
2851
- const { promises: fs } = await import(nodeFsModule);
2852
- const data = await fs.readFile(path);
2853
- const result = await this.createBlobFromData(new Uint8Array(data), {
2854
- ...opts
2855
- });
2886
+ const { createReadStream } = await import(nodeFsModule);
2887
+ const nodeCryptoModule = "node:crypto";
2888
+ const { createHash } = await import(nodeCryptoModule);
2889
+ const nodeStreamModule = "node:stream";
2890
+ const { Readable } = await import(nodeStreamModule);
2891
+ const CHUNK_BYTES = 256 * 1024;
2892
+ let digest = opts?.digest;
2893
+ if (digest === void 0) {
2894
+ const hash = createHash("sha256");
2895
+ const hashStream = createReadStream(path, { highWaterMark: CHUNK_BYTES });
2896
+ try {
2897
+ for await (const chunk of hashStream) {
2898
+ if (opts?.signal?.aborted) {
2899
+ throw new OllamaAbortError("createBlobFromFile: aborted while hashing", {});
2900
+ }
2901
+ hash.update(chunk);
2902
+ }
2903
+ } finally {
2904
+ hashStream.destroy();
2905
+ }
2906
+ digest = `sha256:${hash.digest("hex")}`;
2907
+ }
2908
+ const alreadyExisted = await this.checkBlob(digest, opts);
2909
+ if (!alreadyExisted) {
2910
+ const uploadStream = createReadStream(path, { highWaterMark: CHUNK_BYTES });
2911
+ const onAbort = () => {
2912
+ uploadStream.destroy();
2913
+ };
2914
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
2915
+ try {
2916
+ await this.createBlob(
2917
+ digest,
2918
+ Readable.toWeb(uploadStream),
2919
+ opts
2920
+ );
2921
+ } finally {
2922
+ opts?.signal?.removeEventListener("abort", onAbort);
2923
+ }
2924
+ }
2856
2925
  const fileName = path.split(/[\\/]/).pop() ?? path;
2857
- return { ...result, fileName };
2926
+ return { digest, alreadyExisted, fileName };
2858
2927
  }
2859
2928
  /**
2860
2929
  * One-shot custom-model publishing from raw GGUF weights, following the
@@ -6194,8 +6263,8 @@ var OllamaClient = class _OllamaClient {
6194
6263
  */
6195
6264
  getContextLength = (request) => this.models.getContextLength(request);
6196
6265
  version = () => this.models.version();
6197
- createBlob = (digest, data) => this.models.createBlob(digest, data);
6198
- checkBlob = (digest) => this.models.checkBlob(digest);
6266
+ createBlob = (digest, data, request) => this.models.createBlob(digest, data, request);
6267
+ checkBlob = (digest, request) => this.models.checkBlob(digest, request);
6199
6268
  /**
6200
6269
  * Immediately unloads `model` from VRAM. Convenience alias for
6201
6270
  * {@link ModelsClient.unload} — see that method for the full