@nemesis-oss/ollama-sdk 1.16.0 → 1.18.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.cjs CHANGED
@@ -1113,6 +1113,18 @@ function createRequestId() {
1113
1113
  requestSequence += 1;
1114
1114
  return `ollama-http-${requestSequence}`;
1115
1115
  }
1116
+ function isWebReadableStreamBody(body) {
1117
+ return typeof body === "object" && body !== null && typeof body.getReader === "function";
1118
+ }
1119
+ function buildFetchInit(req) {
1120
+ return {
1121
+ method: req.method,
1122
+ headers: req.headers,
1123
+ ...req.body !== void 0 ? { body: req.body } : {},
1124
+ ...req.signal !== void 0 ? { signal: req.signal } : {},
1125
+ ...isWebReadableStreamBody(req.body) ? { duplex: "half" } : {}
1126
+ };
1127
+ }
1116
1128
  function sameHeaders(actual, expected) {
1117
1129
  const actualEntries = Object.fromEntries(
1118
1130
  Object.entries(actual).map(([key, value]) => [key.toLowerCase(), value])
@@ -1162,13 +1174,7 @@ var HttpClient = class {
1162
1174
  timestamp: startedAt
1163
1175
  });
1164
1176
  const finalHandler = async (req) => {
1165
- const init = {
1166
- method: req.method,
1167
- headers: req.headers,
1168
- ...req.body !== void 0 ? { body: req.body } : {},
1169
- ...req.signal !== void 0 ? { signal: req.signal } : {}
1170
- };
1171
- const response = await this.fetchImpl(req.url, init);
1177
+ const response = await this.fetchImpl(req.url, buildFetchInit(req));
1172
1178
  rawResponse = response;
1173
1179
  const responseHeaders = response.headers && typeof response.headers.entries === "function" ? Object.fromEntries(response.headers.entries()) : {};
1174
1180
  return {
@@ -1179,12 +1185,7 @@ var HttpClient = class {
1179
1185
  };
1180
1186
  try {
1181
1187
  if (this.middleware.length === 0) {
1182
- const response2 = await this.fetchImpl(request.url, {
1183
- method: request.method,
1184
- headers: request.headers,
1185
- ...request.body !== void 0 ? { body: request.body } : {},
1186
- ...request.signal !== void 0 ? { signal: request.signal } : {}
1187
- });
1188
+ const response2 = await this.fetchImpl(request.url, buildFetchInit(request));
1188
1189
  if (response2.status >= 200 && response2.status < 300) {
1189
1190
  this.onLifecycleEvent?.({
1190
1191
  type: "success",
@@ -2775,7 +2776,7 @@ var ModelsClient = class {
2775
2776
  { model, singleEndpoint: true }
2776
2777
  );
2777
2778
  }
2778
- async createBlob(digest, data) {
2779
+ async createBlob(digest, data, request) {
2779
2780
  await this.runner(
2780
2781
  (http, signal) => http.request({
2781
2782
  path: `/api/blobs/${encodeURIComponent(digest)}`,
@@ -2783,10 +2784,10 @@ var ModelsClient = class {
2783
2784
  rawBody: data,
2784
2785
  signal
2785
2786
  }),
2786
- { singleEndpoint: true }
2787
+ { ...request, singleEndpoint: true }
2787
2788
  );
2788
2789
  }
2789
- async checkBlob(digest) {
2790
+ async checkBlob(digest, request) {
2790
2791
  try {
2791
2792
  await this.runner(
2792
2793
  (http, signal) => http.request({
@@ -2794,7 +2795,7 @@ var ModelsClient = class {
2794
2795
  method: "HEAD",
2795
2796
  signal
2796
2797
  }),
2797
- { singleEndpoint: true }
2798
+ { ...request, singleEndpoint: true }
2798
2799
  );
2799
2800
  return true;
2800
2801
  } catch (err) {
@@ -2836,9 +2837,32 @@ var ModelsClient = class {
2836
2837
  */
2837
2838
  async createBlobFromData(data, opts) {
2838
2839
  const digest = opts?.digest ?? await this.computeBlobDigest(data);
2839
- const alreadyExisted = await this.checkBlob(digest);
2840
+ const alreadyExisted = await this.checkBlob(digest, opts);
2840
2841
  if (!alreadyExisted) {
2841
- await this.createBlob(digest, data);
2842
+ await this.createBlob(digest, data, opts);
2843
+ }
2844
+ return { digest, alreadyExisted };
2845
+ }
2846
+ /**
2847
+ * Streams a blob upload straight from a web `ReadableStream` — the
2848
+ * multi-gigabyte-safe counterpart of {@link createBlobFromData}. The body
2849
+ * is never materialized in memory: the transport hands the stream to
2850
+ * `fetch` directly (setting `duplex: 'half'`, which Node 18+ requires for
2851
+ * stream bodies), so peak heap usage is a transport-level chunk, not the
2852
+ * blob size.
2853
+ *
2854
+ * The digest must be known up front — it *is* the upload URL
2855
+ * (`/api/blobs/<digest>`), the same content-addressed protocol
2856
+ * `curl -T` performs. Hash first when you don't have it: in-memory bytes
2857
+ * via {@link computeBlobDigest}, files via {@link createBlobFromFile}
2858
+ * (which hashes in a streaming pass). The server verifies the streamed
2859
+ * bytes against the digest and rejects mismatches, so a corrupted or
2860
+ * truncated stream fails the upload instead of poisoning the store.
2861
+ */
2862
+ async createBlobFromStream(digest, stream, request) {
2863
+ const alreadyExisted = await this.checkBlob(digest, request);
2864
+ if (!alreadyExisted) {
2865
+ await this.createBlob(digest, stream, request);
2842
2866
  }
2843
2867
  return { digest, alreadyExisted };
2844
2868
  }
@@ -2847,16 +2871,61 @@ var ModelsClient = class {
2847
2871
  * `node:fs` import, so browser bundles never pull it in). The file name
2848
2872
  * (basename) is what `/api/create`'s `files` map expects as the key, so
2849
2873
  * this returns it alongside the digest.
2874
+ *
2875
+ * Streams both passes in fixed-size chunks — peak heap is O(chunk), not
2876
+ * O(file), so multi-gigabyte GGUFs never enter the Node heap:
2877
+ *
2878
+ * 1. **Hash pass** (skipped when `opts.digest` is provided): the file is
2879
+ * streamed through `node:crypto`'s incremental SHA-256.
2880
+ * 2. **Upload pass**: `node:fs` createReadStream → web ReadableStream →
2881
+ * `fetch` body with `duplex: 'half'` (see {@link createBlobFromStream}).
2882
+ *
2883
+ * A pre-existing blob (HEAD 200) skips the upload pass entirely, same as
2884
+ * {@link createBlobFromData}.
2850
2885
  */
2851
2886
  async createBlobFromFile(path, opts) {
2852
2887
  const nodeFsModule = "node:fs";
2853
- const { promises: fs } = await import(nodeFsModule);
2854
- const data = await fs.readFile(path);
2855
- const result = await this.createBlobFromData(new Uint8Array(data), {
2856
- ...opts
2857
- });
2888
+ const { createReadStream } = await import(nodeFsModule);
2889
+ const nodeCryptoModule = "node:crypto";
2890
+ const { createHash } = await import(nodeCryptoModule);
2891
+ const nodeStreamModule = "node:stream";
2892
+ const { Readable } = await import(nodeStreamModule);
2893
+ const CHUNK_BYTES = 256 * 1024;
2894
+ let digest = opts?.digest;
2895
+ if (digest === void 0) {
2896
+ const hash = createHash("sha256");
2897
+ const hashStream = createReadStream(path, { highWaterMark: CHUNK_BYTES });
2898
+ try {
2899
+ for await (const chunk of hashStream) {
2900
+ if (opts?.signal?.aborted) {
2901
+ throw new OllamaAbortError("createBlobFromFile: aborted while hashing", {});
2902
+ }
2903
+ hash.update(chunk);
2904
+ }
2905
+ } finally {
2906
+ hashStream.destroy();
2907
+ }
2908
+ digest = `sha256:${hash.digest("hex")}`;
2909
+ }
2910
+ const alreadyExisted = await this.checkBlob(digest, opts);
2911
+ if (!alreadyExisted) {
2912
+ const uploadStream = createReadStream(path, { highWaterMark: CHUNK_BYTES });
2913
+ const onAbort = () => {
2914
+ uploadStream.destroy();
2915
+ };
2916
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
2917
+ try {
2918
+ await this.createBlob(
2919
+ digest,
2920
+ Readable.toWeb(uploadStream),
2921
+ opts
2922
+ );
2923
+ } finally {
2924
+ opts?.signal?.removeEventListener("abort", onAbort);
2925
+ }
2926
+ }
2858
2927
  const fileName = path.split(/[\\/]/).pop() ?? path;
2859
- return { ...result, fileName };
2928
+ return { digest, alreadyExisted, fileName };
2860
2929
  }
2861
2930
  /**
2862
2931
  * One-shot custom-model publishing from raw GGUF weights, following the
@@ -4110,7 +4179,7 @@ var SystemOneNoulQuestionSchema = zod.z.object({
4110
4179
  var SystemOneScoreQuestionSchema = zod.z.object({
4111
4180
  type: zod.z.enum(["score"]),
4112
4181
  instructions: SystemOneContentSchema,
4113
- criteria: zod.z.array(zod.z.string()).min(2).max(26)
4182
+ criteria: zod.z.array(zod.z.string()).min(2).max(10)
4114
4183
  });
4115
4184
 
4116
4185
  // src/generated/models/SystemOneRequest.schema.ts
@@ -4145,7 +4214,7 @@ var SystemOneNoulAnswerSchema = zod.z.object({
4145
4214
  });
4146
4215
  var SystemOneScoreAnswerSchema = zod.z.object({
4147
4216
  type: zod.z.enum(["score"]),
4148
- score: zod.z.number().min(0).max(25),
4217
+ score: zod.z.number().min(0).max(9),
4149
4218
  legend: zod.z.object({}).catchall(zod.z.string()),
4150
4219
  probabilities: SystemOneProbabilitiesSchema,
4151
4220
  confidence: SystemOneConfidenceSchema
@@ -5456,9 +5525,16 @@ var FailoverHttpClient = class {
5456
5525
  };
5457
5526
 
5458
5527
  // src/decision.ts
5528
+ function validateThreshold(value, name) {
5529
+ if (value === void 0) return;
5530
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1) {
5531
+ throw new RangeError(`${name} must be a number in (0, 1] \u2014 got ${String(value)}`);
5532
+ }
5533
+ }
5459
5534
  function createDecision(client) {
5460
5535
  return {
5461
5536
  async choice(params) {
5537
+ validateThreshold(params.minConfidence, "minConfidence");
5462
5538
  const response = await client.systemOne({
5463
5539
  model: params.model,
5464
5540
  state: params.state,
@@ -5477,10 +5553,12 @@ function createDecision(client) {
5477
5553
  choice: answer.choice,
5478
5554
  probabilities: answer.probabilities,
5479
5555
  confidence: answer.confidence,
5556
+ ...params.minConfidence !== void 0 ? { isConfident: answer.confidence >= params.minConfidence } : {},
5480
5557
  usage: response.usage
5481
5558
  };
5482
5559
  },
5483
5560
  async noul(params) {
5561
+ validateThreshold(params.minProbability, "minProbability");
5484
5562
  const response = await client.systemOne({
5485
5563
  model: params.model,
5486
5564
  state: params.state,
@@ -5496,11 +5574,12 @@ function createDecision(client) {
5496
5574
  const answer = response.answers.decision;
5497
5575
  return {
5498
5576
  noul: answer.noul,
5499
- bool: answer.noul >= 0.5,
5577
+ bool: answer.noul >= (params.minProbability ?? 0.5),
5500
5578
  usage: response.usage
5501
5579
  };
5502
5580
  },
5503
5581
  async score(params) {
5582
+ validateThreshold(params.minConfidence, "minConfidence");
5504
5583
  const response = await client.systemOne({
5505
5584
  model: params.model,
5506
5585
  state: params.state,
@@ -5520,10 +5599,12 @@ function createDecision(client) {
5520
5599
  legend: answer.legend,
5521
5600
  probabilities: answer.probabilities,
5522
5601
  confidence: answer.confidence,
5602
+ ...params.minConfidence !== void 0 ? { isConfident: answer.confidence >= params.minConfidence } : {},
5523
5603
  usage: response.usage
5524
5604
  };
5525
5605
  },
5526
5606
  async route(params) {
5607
+ validateThreshold(params.minConfidence, "minConfidence");
5527
5608
  const response = await client.systemOne({
5528
5609
  model: params.model,
5529
5610
  state: params.state,
@@ -5540,10 +5621,12 @@ function createDecision(client) {
5540
5621
  const answer = response.answers.decision;
5541
5622
  return {
5542
5623
  route: answer.choice,
5543
- confidence: answer.confidence
5624
+ confidence: answer.confidence,
5625
+ ...params.minConfidence !== void 0 ? { isConfident: answer.confidence >= params.minConfidence } : {}
5544
5626
  };
5545
5627
  },
5546
5628
  async verify(params) {
5629
+ validateThreshold(params.minProbability, "minProbability");
5547
5630
  const state = {
5548
5631
  claim: params.claim,
5549
5632
  evidence: params.evidence
@@ -5562,11 +5645,12 @@ function createDecision(client) {
5562
5645
  });
5563
5646
  const answer = response.answers.decision;
5564
5647
  return {
5565
- verified: answer.noul >= 0.5,
5648
+ verified: answer.noul >= (params.minProbability ?? 0.5),
5566
5649
  probability: answer.noul
5567
5650
  };
5568
5651
  },
5569
5652
  async rank(params) {
5653
+ validateThreshold(params.minConfidence, "minConfidence");
5570
5654
  const promises = params.candidates.map(async (candidate) => {
5571
5655
  const response = await client.systemOne({
5572
5656
  model: params.model,
@@ -5585,7 +5669,8 @@ function createDecision(client) {
5585
5669
  return {
5586
5670
  candidate: candidate.id,
5587
5671
  score: answer.score,
5588
- confidence: answer.confidence
5672
+ confidence: answer.confidence,
5673
+ ...params.minConfidence !== void 0 ? { isConfident: answer.confidence >= params.minConfidence } : {}
5589
5674
  };
5590
5675
  });
5591
5676
  const results = await Promise.all(promises);
@@ -6196,8 +6281,8 @@ var OllamaClient = class _OllamaClient {
6196
6281
  */
6197
6282
  getContextLength = (request) => this.models.getContextLength(request);
6198
6283
  version = () => this.models.version();
6199
- createBlob = (digest, data) => this.models.createBlob(digest, data);
6200
- checkBlob = (digest) => this.models.checkBlob(digest);
6284
+ createBlob = (digest, data, request) => this.models.createBlob(digest, data, request);
6285
+ checkBlob = (digest, request) => this.models.checkBlob(digest, request);
6201
6286
  /**
6202
6287
  * Immediately unloads `model` from VRAM. Convenience alias for
6203
6288
  * {@link ModelsClient.unload} — see that method for the full
@@ -6717,6 +6802,64 @@ function toResponse(stream, init = {}) {
6717
6802
  return new Response(body, { ...init, headers });
6718
6803
  }
6719
6804
 
6805
+ // src/streaming/progress.ts
6806
+ function computeProgressPercent(chunk) {
6807
+ if (typeof chunk.total !== "number" || typeof chunk.completed !== "number") return void 0;
6808
+ if (chunk.total <= 0) return void 0;
6809
+ const ratio = chunk.completed / chunk.total;
6810
+ const clamped = Math.max(0, Math.min(1, ratio));
6811
+ return Math.round(clamped * 1e4) / 100;
6812
+ }
6813
+ function toPullProgressEvent(chunk) {
6814
+ return {
6815
+ status: chunk.status,
6816
+ ...chunk.digest !== void 0 ? { digest: chunk.digest } : {},
6817
+ ...chunk.total !== void 0 ? { total: chunk.total } : {},
6818
+ ...chunk.completed !== void 0 ? { completed: chunk.completed } : {},
6819
+ percent: computeProgressPercent(chunk),
6820
+ raw: chunk
6821
+ };
6822
+ }
6823
+ function createProgressSpeedTracker() {
6824
+ let epochDigest;
6825
+ let epochStartMs = 0;
6826
+ let epochStartCompleted = 0;
6827
+ return (chunk) => {
6828
+ if (typeof chunk.completed !== "number") return {};
6829
+ const digestChanged = chunk.digest !== void 0 && chunk.digest !== epochDigest;
6830
+ const regressed = chunk.completed < epochStartCompleted;
6831
+ if (digestChanged || regressed) {
6832
+ epochDigest = chunk.digest;
6833
+ epochStartMs = performance.now();
6834
+ epochStartCompleted = chunk.completed;
6835
+ return {};
6836
+ }
6837
+ const elapsedSeconds = (performance.now() - epochStartMs) / 1e3;
6838
+ if (elapsedSeconds <= 0) return {};
6839
+ const transferred = chunk.completed - epochStartCompleted;
6840
+ if (transferred <= 0) return {};
6841
+ const bytesPerSecond = transferred / elapsedSeconds;
6842
+ const etaSeconds = typeof chunk.total === "number" ? Math.max(0, (chunk.total - chunk.completed) / bytesPerSecond) : void 0;
6843
+ return { bytesPerSecond, etaSeconds };
6844
+ };
6845
+ }
6846
+ async function onProgress(stream, callback) {
6847
+ void stream.finalResult.catch(() => void 0);
6848
+ const speed = createProgressSpeedTracker();
6849
+ for await (const event of stream) {
6850
+ if (event.type === "message") {
6851
+ callback({ ...toPullProgressEvent(event.data.chunk), ...speed(event.data.chunk) });
6852
+ }
6853
+ if (event.type === "done") {
6854
+ return event.data.result;
6855
+ }
6856
+ if (event.type === "error") {
6857
+ throw event.data.error;
6858
+ }
6859
+ }
6860
+ throw new OllamaStreamError("Pull/push stream ended before reporting success");
6861
+ }
6862
+
6720
6863
  // src/streaming/observe.ts
6721
6864
  async function* observeStream(source, callbacks = {}) {
6722
6865
  const { onChunk, onError, onDone, onAbandon, onListenerError } = callbacks;
@@ -8220,11 +8363,13 @@ exports.checkGenerateContext = checkGenerateContext;
8220
8363
  exports.compactConversationHistory = compactConversationHistory;
8221
8364
  exports.compareDraftPerformance = compareDraftPerformance;
8222
8365
  exports.composeMiddleware = composeMiddleware;
8366
+ exports.computeProgressPercent = computeProgressPercent;
8223
8367
  exports.contextWarningMessage = contextWarningMessage;
8224
8368
  exports.createConsoleLogger = createConsoleLogger;
8225
8369
  exports.createDecision = createDecision;
8226
8370
  exports.createModelAffinityScheduler = createModelAffinityScheduler;
8227
8371
  exports.createOllamaCloudFreeTierQuota = createOllamaCloudFreeTierQuota;
8372
+ exports.createProgressSpeedTracker = createProgressSpeedTracker;
8228
8373
  exports.createTimeoutSignal = createTimeoutSignal;
8229
8374
  exports.createWebFetchTool = createWebFetchTool;
8230
8375
  exports.createWebSearchTool = createWebSearchTool;
@@ -8266,6 +8411,7 @@ exports.normalizeGenerateStream = normalizeGenerateStream;
8266
8411
  exports.normalizeKeepAlive = normalizeKeepAlive;
8267
8412
  exports.normalizeProgressStream = normalizeProgressStream;
8268
8413
  exports.observeStream = observeStream;
8414
+ exports.onProgress = onProgress;
8269
8415
  exports.parseFrontmatter = parseFrontmatter;
8270
8416
  exports.parseNdjsonStream = parseNdjsonStream;
8271
8417
  exports.parseSseStream = parseSseStream;
@@ -8278,6 +8424,7 @@ exports.resolveImageInput = resolveImageInput;
8278
8424
  exports.sanitizeHistoryForNextTurn = sanitizeHistoryForNextTurn;
8279
8425
  exports.setSpanError = setSpanError;
8280
8426
  exports.toDataStream = toDataStream;
8427
+ exports.toPullProgressEvent = toPullProgressEvent;
8281
8428
  exports.toResponse = toResponse;
8282
8429
  exports.toTextStream = toTextStream;
8283
8430
  exports.withRetry = withRetry;