@nemesis-oss/ollama-sdk 1.8.0 → 1.10.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/README.md +247 -11
  3. package/dist/{anthropic-B_UXplNF.d.ts → anthropic-CPaoxeLX.d.ts} +4 -2
  4. package/dist/{anthropic-CVDFy_a0.d.cts → anthropic-DuE8M6Tz.d.cts} +4 -2
  5. package/dist/{compose-DLStUfaF.d.ts → compose-BQYXdpar.d.ts} +1 -1
  6. package/dist/{compose-DS-UWFx8.d.cts → compose-lt-Qz3lI.d.cts} +1 -1
  7. package/dist/generated-api.cjs +42 -3
  8. package/dist/generated-api.cjs.map +1 -1
  9. package/dist/generated-api.d.cts +96 -6
  10. package/dist/generated-api.d.ts +96 -6
  11. package/dist/generated-api.js +41 -4
  12. package/dist/generated-api.js.map +1 -1
  13. package/dist/generated-models-schemas.cjs +62 -2
  14. package/dist/generated-models-schemas.cjs.map +1 -1
  15. package/dist/generated-models-schemas.d.cts +108 -3
  16. package/dist/generated-models-schemas.d.ts +108 -3
  17. package/dist/generated-models-schemas.js +56 -3
  18. package/dist/generated-models-schemas.js.map +1 -1
  19. package/dist/generated-runtime.cjs +105 -5
  20. package/dist/generated-runtime.cjs.map +1 -1
  21. package/dist/generated-runtime.d.cts +2 -2
  22. package/dist/generated-runtime.d.ts +2 -2
  23. package/dist/generated-runtime.js +105 -5
  24. package/dist/generated-runtime.js.map +1 -1
  25. package/dist/index.cjs +1644 -54
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +1506 -14
  28. package/dist/index.d.ts +1506 -14
  29. package/dist/index.js +1607 -55
  30. package/dist/index.js.map +1 -1
  31. package/dist/mcp/tools.json +43 -24
  32. package/dist/mcp-generated.cjs +43 -1
  33. package/dist/mcp-generated.cjs.map +1 -1
  34. package/dist/mcp-generated.d.cts +2 -2
  35. package/dist/mcp-generated.d.ts +2 -2
  36. package/dist/mcp-generated.js +43 -1
  37. package/dist/mcp-generated.js.map +1 -1
  38. package/dist/{runtime-C6UYtBRB.d.ts → runtime-BV9SK_cf.d.ts} +69 -2
  39. package/dist/{runtime-DTmUeU6K.d.cts → runtime-BsdBbuOv.d.cts} +69 -2
  40. package/dist/skills.cjs.map +1 -1
  41. package/dist/skills.d.cts +3 -3
  42. package/dist/skills.d.ts +3 -3
  43. package/dist/skills.js.map +1 -1
  44. package/dist/{types-5PAjLoCp.d.cts → types-BtQexFpT.d.cts} +113 -10
  45. package/dist/{types-5PAjLoCp.d.ts → types-BtQexFpT.d.ts} +113 -10
  46. package/package.json +11 -6
package/dist/index.js CHANGED
@@ -46,6 +46,47 @@ var __callDispose = (stack, error, hasError) => {
46
46
  return next();
47
47
  };
48
48
 
49
+ // src/transport/host.ts
50
+ var DEFAULT_PORT = "11434";
51
+ var SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
52
+ function normalizeBaseUrl(host) {
53
+ const trimmed = host.trim();
54
+ if (trimmed === "") return trimmed;
55
+ const implicitScheme = !SCHEME_RE.test(trimmed);
56
+ const withHost = trimmed.startsWith(":") ? `127.0.0.1${trimmed}` : trimmed;
57
+ const candidate = implicitScheme ? `http://${withHost}` : withHost;
58
+ let url;
59
+ try {
60
+ url = new URL(candidate);
61
+ } catch {
62
+ return trimmed.replace(/\/+$/, "");
63
+ }
64
+ let protocol = url.protocol;
65
+ let port = url.port;
66
+ if (implicitScheme) {
67
+ if (port === "443") {
68
+ protocol = "https:";
69
+ port = "";
70
+ } else if (port === "") {
71
+ port = DEFAULT_PORT;
72
+ }
73
+ }
74
+ const authority = `${url.hostname}${port !== "" ? `:${port}` : ""}`;
75
+ return `${protocol}//${authority}${url.pathname}`.replace(/\/+$/, "");
76
+ }
77
+ function isEnvApiKeyHost(baseUrl) {
78
+ let hostname;
79
+ try {
80
+ hostname = new URL(normalizeBaseUrl(baseUrl)).hostname.toLowerCase();
81
+ } catch {
82
+ return false;
83
+ }
84
+ if (hostname === "ollama.com" || hostname.endsWith(".ollama.com")) return true;
85
+ if (hostname === "localhost" || hostname.endsWith(".localhost")) return true;
86
+ if (hostname === "[::1]" || hostname === "::1" || hostname === "0.0.0.0") return true;
87
+ return /^127(?:\.\d{1,3}){3}$/.test(hostname);
88
+ }
89
+
49
90
  // src/config.ts
50
91
  var DEFAULT_BASE_URL = "http://localhost:11434";
51
92
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -54,11 +95,16 @@ function readEnv(name) {
54
95
  return typeof process !== "undefined" && process.env ? process.env[name] : void 0;
55
96
  }
56
97
  function resolveBaseUrl(configBaseUrl) {
57
- return configBaseUrl ?? readEnv("OLLAMA_HOST") ?? DEFAULT_BASE_URL;
98
+ return normalizeBaseUrl(configBaseUrl ?? readEnv("OLLAMA_HOST") ?? DEFAULT_BASE_URL);
58
99
  }
59
100
  function resolveApiKey(configApiKey) {
60
101
  return configApiKey ?? readEnv("OLLAMA_API_KEY");
61
102
  }
103
+ function resolveEndpointApiKey(configApiKey, baseUrl) {
104
+ if (configApiKey !== void 0) return configApiKey;
105
+ const envKey = readEnv("OLLAMA_API_KEY");
106
+ return envKey !== void 0 && isEnvApiKeyHost(baseUrl) ? envKey : void 0;
107
+ }
62
108
  function resolveCredentialEndpoints(config) {
63
109
  if (config.credentials === void 0) return [];
64
110
  const modelsByCredential = /* @__PURE__ */ new Map();
@@ -91,6 +137,18 @@ var DEFAULT_FAILOVER_CODES = [
91
137
  "network_error",
92
138
  "timeout",
93
139
  "server_error",
140
+ // HTTP 503 — server queue saturated (OLLAMA_MAX_QUEUE exceeded).
141
+ // The new OllamaOverloadedError specialization is retryable: the
142
+ // queue will eventually drain, and a different candidate endpoint
143
+ // may have capacity right now. Listed separately from
144
+ // 'server_error' because OllamaOverloadedError extends
145
+ // OllamaClientError (NOT OllamaServerError) — see
146
+ // src/errors.ts for the rationale.
147
+ "overloaded",
148
+ // HTTP 502 — cloud model could not be reached by the Ollama Cloud
149
+ // proxy. Fail over to a different model/endpoint rather than
150
+ // retrying the same one.
151
+ "bad_gateway",
94
152
  "rate_limited",
95
153
  "auth_error",
96
154
  // A candidate endpoint rejected the request pre-flight for a known-unsupported
@@ -159,6 +217,24 @@ var OllamaServerError = class extends OllamaClientError {
159
217
  super(message, { ...options, code: "server_error", retryable: true });
160
218
  }
161
219
  };
220
+ var OllamaOverloadedError = class extends OllamaClientError {
221
+ constructor(message, options) {
222
+ super(message, { ...options, code: "overloaded", status: 503, retryable: true });
223
+ }
224
+ };
225
+ var OllamaBadGatewayError = class extends OllamaClientError {
226
+ constructor(message, options) {
227
+ super(message, { ...options, code: "bad_gateway", status: 502, retryable: true });
228
+ }
229
+ };
230
+ var OllamaStreamError = class extends OllamaClientError {
231
+ /** Content accumulated before the in-band error frame, or '' if none. */
232
+ partialContent;
233
+ constructor(message, options) {
234
+ super(message, { ...options, code: "stream_error", retryable: false });
235
+ this.partialContent = options?.partialContent ?? "";
236
+ }
237
+ };
162
238
  var OllamaAbortError = class extends OllamaClientError {
163
239
  constructor(message, options) {
164
240
  super(message, { ...options, code: "aborted", retryable: false });
@@ -308,6 +384,12 @@ function statusToError(status, message, options) {
308
384
  if (status === 429) {
309
385
  return new OllamaRateLimitError(message, { ...options, status });
310
386
  }
387
+ if (status === 502) {
388
+ return new OllamaBadGatewayError(message, { ...options });
389
+ }
390
+ if (status === 503) {
391
+ return new OllamaOverloadedError(message, { ...options });
392
+ }
311
393
  if (status >= 500) {
312
394
  return new OllamaServerError(message, { ...options, status });
313
395
  }
@@ -578,6 +660,75 @@ function composeMiddleware(middlewares, finalHandler) {
578
660
  };
579
661
  }
580
662
 
663
+ // src/vision.ts
664
+ var DATA_URI_PATTERN = /^data:image\/[a-z0-9.+-]+;base64,/i;
665
+ var IMAGE_FILE_EXTENSION_PATTERN = /\.(png|jpe?g|webp|gif|bmp|tiff?|avif)$/i;
666
+ function imageStringNeedsResolution(input) {
667
+ return DATA_URI_PATTERN.test(input) || input.startsWith("http://") || input.startsWith("https://") || IMAGE_FILE_EXTENSION_PATTERN.test(input);
668
+ }
669
+ function hasNodeFs() {
670
+ return typeof process !== "undefined" && process !== null && typeof process.versions?.node === "string";
671
+ }
672
+ var NODE_FS_MODULE = "node:fs";
673
+ async function readFileAsBase64(path) {
674
+ const { promises: fs } = await import(NODE_FS_MODULE);
675
+ const data = await fs.readFile(path);
676
+ return data.toString("base64");
677
+ }
678
+ async function fetchUrlAsBase64(url, signal) {
679
+ const res = await fetch(url, signal !== void 0 ? { signal } : void 0);
680
+ if (!res.ok) {
681
+ throw new OllamaClientError(
682
+ `Failed to fetch image from URL "${url}": HTTP ${res.status} ${res.statusText}.`,
683
+ { code: "image_fetch_failed" }
684
+ );
685
+ }
686
+ const arrayBuffer = await res.arrayBuffer();
687
+ return encodeBytesBase64(new Uint8Array(arrayBuffer));
688
+ }
689
+ function encodeBytesBase64(bytes) {
690
+ const bufferCtor = globalThis.Buffer;
691
+ if (bufferCtor !== void 0) return bufferCtor.from(bytes).toString("base64");
692
+ const CHUNK_SIZE = 32768;
693
+ let binary = "";
694
+ for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
695
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_SIZE));
696
+ }
697
+ return btoa(binary);
698
+ }
699
+ async function resolveImageInput(input, signal) {
700
+ if (typeof input !== "string") {
701
+ return encodeBytesBase64(input instanceof Uint8Array ? input : new Uint8Array(input));
702
+ }
703
+ if (DATA_URI_PATTERN.test(input)) {
704
+ return input.replace(DATA_URI_PATTERN, "").trim();
705
+ }
706
+ if (input.startsWith("http://") || input.startsWith("https://")) {
707
+ return fetchUrlAsBase64(input, signal);
708
+ }
709
+ if (IMAGE_FILE_EXTENSION_PATTERN.test(input)) {
710
+ if (!hasNodeFs()) {
711
+ throw new OllamaClientError(
712
+ `Vision input "${input}" looks like a local file path, but this runtime has no Node.js \`fs\` module (browser/edge). Read the file into bytes yourself and pass the \`Uint8Array\`, or pass a base64 string / data URI / http(s) URL instead.`,
713
+ { code: "vision_path_unsupported_runtime" }
714
+ );
715
+ }
716
+ try {
717
+ return await readFileAsBase64(input);
718
+ } catch {
719
+ return input;
720
+ }
721
+ }
722
+ return input.trim();
723
+ }
724
+ async function resolveImages(images, signal) {
725
+ if (images === void 0) return void 0;
726
+ if (images.every((image) => typeof image === "string" && !imageStringNeedsResolution(image))) {
727
+ return images;
728
+ }
729
+ return Promise.all(images.map((image) => resolveImageInput(image, signal)));
730
+ }
731
+
581
732
  // src/utils.ts
582
733
  async function encodeImage(image) {
583
734
  if (typeof image === "string") return image;
@@ -590,18 +741,22 @@ async function encodeImage(image) {
590
741
  }
591
742
  return btoa(binary);
592
743
  }
593
- async function encodeImages(images) {
594
- if (images === void 0) return void 0;
595
- return Promise.all(images.map((image) => encodeImage(image)));
744
+ async function encodeImages(images, signal) {
745
+ return resolveImages(images, signal);
596
746
  }
597
- async function withEncodedImages(req) {
598
- if (!req.images?.some((image) => image instanceof Uint8Array)) return req;
599
- return { ...req, images: await encodeImages(req.images) };
747
+ async function withEncodedImages(req, signal) {
748
+ const needsResolution = req.images?.some(
749
+ (image) => typeof image !== "string" || imageStringNeedsResolution(image)
750
+ );
751
+ if (!needsResolution) return req;
752
+ return { ...req, images: await encodeImages(req.images ?? [], signal) };
600
753
  }
601
- async function withEncodedMessageImages(messages) {
602
- if (!messages.some((message) => message.images?.some((image) => image instanceof Uint8Array)))
603
- return messages;
604
- return Promise.all(messages.map((message) => withEncodedImages(message)));
754
+ async function withEncodedMessageImages(messages, signal) {
755
+ const needsResolution = messages.some(
756
+ (message) => message.images?.some((image) => typeof image !== "string" || imageStringNeedsResolution(image))
757
+ );
758
+ if (!needsResolution) return messages;
759
+ return Promise.all(messages.map((message) => withEncodedImages(message, signal)));
605
760
  }
606
761
  function disposableTimer(timer) {
607
762
  return {
@@ -630,6 +785,12 @@ function disposableSpan(span) {
630
785
  }
631
786
 
632
787
  // src/streaming/ndjson.ts
788
+ function isInBandErrorFrame(value) {
789
+ if (typeof value !== "object" || value === null) return false;
790
+ const record = value;
791
+ if (typeof record.error !== "string" || record.error.length === 0) return false;
792
+ return !("model" in record) && !("response" in record) && !("message" in record) && !("status" in record) && !("done" in record);
793
+ }
633
794
  async function* parseNdjsonStream(stream) {
634
795
  var _stack = [];
635
796
  try {
@@ -646,17 +807,34 @@ async function* parseNdjsonStream(stream) {
646
807
  for (const line of lines) {
647
808
  const trimmed = line.trim();
648
809
  if (!trimmed) continue;
810
+ let json;
649
811
  try {
650
- yield JSON.parse(trimmed);
812
+ json = JSON.parse(trimmed);
651
813
  } catch (err) {
652
814
  throw new OllamaGenericClientError(`Failed to parse stream JSON chunk: ${trimmed}`, {
653
815
  cause: err
654
816
  });
655
817
  }
818
+ if (isInBandErrorFrame(json)) {
819
+ throw new OllamaStreamError(json.error);
820
+ }
821
+ yield json;
656
822
  }
657
823
  }
658
824
  if (buffer.trim()) {
659
- yield JSON.parse(buffer.trim());
825
+ const trimmed = buffer.trim();
826
+ let json;
827
+ try {
828
+ json = JSON.parse(trimmed);
829
+ } catch (err) {
830
+ throw new OllamaGenericClientError(`Failed to parse stream JSON chunk: ${trimmed}`, {
831
+ cause: err
832
+ });
833
+ }
834
+ if (isInBandErrorFrame(json)) {
835
+ throw new OllamaStreamError(json.error);
836
+ }
837
+ yield json;
660
838
  }
661
839
  } catch (_) {
662
840
  var _error = _, _hasError = true;
@@ -867,7 +1045,7 @@ var HttpClient = class {
867
1045
  onLifecycleEvent;
868
1046
  requestId;
869
1047
  constructor(options) {
870
- this.baseUrl = options.baseUrl.replace(/\/+$/, "");
1048
+ this.baseUrl = normalizeBaseUrl(options.baseUrl);
871
1049
  this.apiKey = options.apiKey;
872
1050
  this.defaultHeaders = options.headers ?? {};
873
1051
  this.fetchImpl = options.fetch ?? globalThis.fetch;
@@ -1256,7 +1434,7 @@ async function checkEndpointHealth(endpoint, fetchImpl, timeoutMs = 5e3, middlew
1256
1434
  function inferRuntimeMode(baseUrl) {
1257
1435
  try {
1258
1436
  const url = new URL(baseUrl);
1259
- const host = url.hostname.toLowerCase();
1437
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
1260
1438
  if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.startsWith("192.168.") || host.startsWith("10.")) {
1261
1439
  return "local";
1262
1440
  }
@@ -1274,6 +1452,63 @@ function extractContextLength(modelInfo) {
1274
1452
  }
1275
1453
  return void 0;
1276
1454
  }
1455
+ function extractModelFamily(showRes) {
1456
+ if (showRes.model_info) {
1457
+ for (const [key, value] of Object.entries(showRes.model_info)) {
1458
+ if (key.endsWith(".architecture") && typeof value === "string" && value.length > 0) {
1459
+ return value.toLowerCase();
1460
+ }
1461
+ }
1462
+ }
1463
+ if (showRes.details?.family && showRes.details.family.length > 0) {
1464
+ return showRes.details.family.toLowerCase();
1465
+ }
1466
+ const families = showRes.details?.families;
1467
+ if (families && families.length > 0) {
1468
+ const first = families[0];
1469
+ if (typeof first === "string" && first.length > 0) return first.toLowerCase();
1470
+ }
1471
+ return void 0;
1472
+ }
1473
+ var PARALLEL_TOOL_CALL_LOCAL_FAMILIES = /* @__PURE__ */ new Set([
1474
+ "qwen2",
1475
+ "qwen2.5",
1476
+ "qwen3",
1477
+ "llama3.1",
1478
+ "llama3.2",
1479
+ "llama3.3",
1480
+ "llama4",
1481
+ "mistral",
1482
+ "mixtral",
1483
+ "hermes",
1484
+ "command-r",
1485
+ "command-r-plus"
1486
+ ]);
1487
+ var PARALLEL_TOOL_CALL_NAME_HINTS = /* @__PURE__ */ new Set([
1488
+ "tool-use",
1489
+ "tooluse",
1490
+ "function-call",
1491
+ "functioncall",
1492
+ "instruct",
1493
+ // Most "instruct" variants of tool-capable families ship with parallel tool calls
1494
+ "hermes",
1495
+ "command-r"
1496
+ ]);
1497
+ function inferLocalParallelToolCalls(model, family) {
1498
+ const modelLower = model.toLowerCase();
1499
+ if (family !== void 0 && PARALLEL_TOOL_CALL_LOCAL_FAMILIES.has(family)) {
1500
+ return "yes";
1501
+ }
1502
+ for (const hint of PARALLEL_TOOL_CALL_NAME_HINTS) {
1503
+ if (modelLower.includes(hint)) {
1504
+ return "yes";
1505
+ }
1506
+ }
1507
+ return "unknown";
1508
+ }
1509
+ function inferCloudParallelToolCalls(supportsTools) {
1510
+ return supportsTools ? "yes" : "unknown";
1511
+ }
1277
1512
  async function detectModelCapabilities(http, model, signal) {
1278
1513
  const showRes = await http.request({
1279
1514
  path: "/api/show",
@@ -1283,10 +1518,16 @@ async function detectModelCapabilities(http, model, signal) {
1283
1518
  const reported = showRes.capabilities ?? [];
1284
1519
  const reportedSet = new Set(reported.map((c) => c.toLowerCase()));
1285
1520
  const contextLength = extractContextLength(showRes.model_info);
1521
+ const supportsTools = reportedSet.has("tools");
1522
+ const runtimeMode = inferRuntimeMode(http.baseUrl);
1523
+ const parallelToolCalls = runtimeMode === "cloud" ? inferCloudParallelToolCalls(supportsTools) : runtimeMode === "local" ? inferLocalParallelToolCalls(model, extractModelFamily(showRes)) : (
1524
+ // unknown runtime mode — can't even tell if we're cloud or local
1525
+ "unknown"
1526
+ );
1286
1527
  return {
1287
1528
  model,
1288
1529
  reported,
1289
- supportsTools: reportedSet.has("tools"),
1530
+ supportsTools,
1290
1531
  supportsVision: reportedSet.has("vision"),
1291
1532
  supportsEmbedding: reportedSet.has("embedding"),
1292
1533
  supportsCompletion: reportedSet.has("completion") || !reportedSet.has("embedding"),
@@ -1294,7 +1535,8 @@ async function detectModelCapabilities(http, model, signal) {
1294
1535
  ...showRes.thinking !== void 0 ? { thinking: showRes.thinking } : {},
1295
1536
  ...contextLength !== void 0 ? { contextLength } : {},
1296
1537
  supportsStreaming: true,
1297
- supportsStructuredOutputRequest: inferRuntimeMode(http.baseUrl) !== "cloud"
1538
+ supportsStructuredOutputRequest: runtimeMode !== "cloud",
1539
+ parallelToolCalls
1298
1540
  };
1299
1541
  }
1300
1542
  async function listAvailableModels(http) {
@@ -1439,7 +1681,131 @@ function ensureToolCallIds(toolCalls) {
1439
1681
  return toolCalls.map((tc) => tc.id !== void 0 ? tc : { ...tc, id: generateToolCallId() });
1440
1682
  }
1441
1683
 
1684
+ // src/tools/tool-call-accumulator.ts
1685
+ function isSameToolCall(a, b, aIndex, bIndex) {
1686
+ if (a.id !== void 0 && b.id !== void 0) {
1687
+ return a.id === b.id;
1688
+ }
1689
+ if (aIndex !== void 0 && bIndex !== void 0) {
1690
+ return aIndex === bIndex;
1691
+ }
1692
+ return false;
1693
+ }
1694
+ function mergeToolCall(earlier, later) {
1695
+ const earlierFn = earlier.function;
1696
+ const laterFn = later.function;
1697
+ const earlierArgs = earlierFn.arguments;
1698
+ const laterArgs = laterFn.arguments;
1699
+ const mergedFn = {
1700
+ name: laterFn.name || earlierFn.name,
1701
+ arguments: earlierArgs !== void 0 && laterArgs !== void 0 ? { ...earlierArgs, ...laterArgs } : laterArgs ?? earlierArgs ?? {}
1702
+ };
1703
+ return {
1704
+ // id: prefer the later one if present, else the earlier one
1705
+ ...later.id !== void 0 ? { id: later.id } : earlier.id !== void 0 ? { id: earlier.id } : {},
1706
+ function: mergedFn
1707
+ };
1708
+ }
1709
+ function mergeToolCallArgumentsString(earlier, later) {
1710
+ const earlierArgs = earlier.function.arguments;
1711
+ const laterArgs = later.function.arguments;
1712
+ const concatenated = (typeof earlierArgs === "string" ? earlierArgs : "") + (typeof laterArgs === "string" ? laterArgs : "");
1713
+ return {
1714
+ ...later.id !== void 0 ? { id: later.id } : earlier.id !== void 0 ? { id: earlier.id } : {},
1715
+ function: {
1716
+ name: later.function.name || earlier.function.name,
1717
+ arguments: concatenated
1718
+ }
1719
+ };
1720
+ }
1721
+ var ToolCallAccumulator = class {
1722
+ entries = [];
1723
+ /**
1724
+ * Process a chunk's `tool_calls` array. Entries that match an
1725
+ * existing accumulated entry (by id or array position) are merged;
1726
+ * new entries are appended.
1727
+ *
1728
+ * Returns the current accumulated state (a shallow copy — entries
1729
+ * themselves are NOT deep-cloned, so callers that mutate
1730
+ * `function.arguments` on a returned entry will mutate the
1731
+ * accumulator's internal state. Use {@link snapshot} for an
1732
+ * explicitly read-only view, or deep-clone the result if you need
1733
+ * full isolation).
1734
+ */
1735
+ process(toolCalls) {
1736
+ if (!toolCalls || toolCalls.length === 0) {
1737
+ return [...this.entries];
1738
+ }
1739
+ for (let i = 0; i < toolCalls.length; i++) {
1740
+ const incoming = toolCalls[i];
1741
+ if (!incoming) continue;
1742
+ let matchedIndex = -1;
1743
+ for (let j = 0; j < this.entries.length; j++) {
1744
+ const existing = this.entries[j];
1745
+ if (existing && isSameToolCall(existing, incoming, j, i)) {
1746
+ matchedIndex = j;
1747
+ break;
1748
+ }
1749
+ }
1750
+ if (matchedIndex >= 0) {
1751
+ this.entries[matchedIndex] = mergeToolCall(this.entries[matchedIndex], incoming);
1752
+ } else {
1753
+ this.entries.push(incoming);
1754
+ }
1755
+ }
1756
+ return [...this.entries];
1757
+ }
1758
+ /**
1759
+ * Returns the final accumulated tool calls. Identical to calling
1760
+ * `process(undefined)` — included for ergonomics so callers can
1761
+ * express "I'm done, give me the result" without a no-op call.
1762
+ *
1763
+ * The returned array is a shallow copy; entries themselves are
1764
+ * the same object references the accumulator holds internally.
1765
+ */
1766
+ finalize() {
1767
+ return [...this.entries];
1768
+ }
1769
+ /**
1770
+ * Returns the current accumulated state without modifying the
1771
+ * accumulator. Useful for mid-stream inspection.
1772
+ *
1773
+ * The returned array is a shallow copy; mutating it (e.g.
1774
+ * `result.push(...)`) does not affect the accumulator, but
1775
+ * mutating an entry's `function.arguments` object DOES affect
1776
+ * the accumulator's internal state because the entry reference
1777
+ * is shared. Deep-clone the result if you need full isolation.
1778
+ */
1779
+ snapshot() {
1780
+ return [...this.entries];
1781
+ }
1782
+ /**
1783
+ * Returns the number of accumulated tool calls so far.
1784
+ */
1785
+ get length() {
1786
+ return this.entries.length;
1787
+ }
1788
+ };
1789
+ function mergeToolCallArrays(earlier, later) {
1790
+ const acc = new ToolCallAccumulator();
1791
+ acc.process(earlier);
1792
+ acc.process(later);
1793
+ return acc.finalize();
1794
+ }
1795
+
1442
1796
  // src/streaming/stream.ts
1797
+ function extractPartialContent(accumulated) {
1798
+ if (typeof accumulated !== "object" || accumulated === null) return "";
1799
+ const record = accumulated;
1800
+ const message = record.message;
1801
+ if (typeof message === "object" && message !== null) {
1802
+ const content = message.content;
1803
+ if (typeof content === "string") return content;
1804
+ }
1805
+ const response = record.response;
1806
+ if (typeof response === "string") return response;
1807
+ return "";
1808
+ }
1443
1809
  var OllamaStream = class {
1444
1810
  constructor(source, mapChunk, aggregate, initial, signal) {
1445
1811
  this.source = source;
@@ -1460,10 +1826,6 @@ var OllamaStream = class {
1460
1826
  }
1461
1827
  }
1462
1828
  }
1463
- source;
1464
- mapChunk;
1465
- aggregate;
1466
- initial;
1467
1829
  mode = "unconsumed";
1468
1830
  listeners = /* @__PURE__ */ new Map();
1469
1831
  finalResultPromise;
@@ -1522,7 +1884,10 @@ var OllamaStream = class {
1522
1884
  } catch (error) {
1523
1885
  this.removeAbortListener?.();
1524
1886
  this.removeAbortListener = void 0;
1525
- const mapped = mapError(error);
1887
+ const mapped = error instanceof OllamaStreamError && error.partialContent === "" ? new OllamaStreamError(error.message, {
1888
+ partialContent: extractPartialContent(accumulated),
1889
+ cause: error
1890
+ }) : mapError(error);
1526
1891
  this.emit({ type: "error", data: { error: mapped } });
1527
1892
  this.rejectFinal(mapped);
1528
1893
  }
@@ -1549,7 +1914,10 @@ var OllamaStream = class {
1549
1914
  }
1550
1915
  }
1551
1916
  } catch (error) {
1552
- const mapped = mapError(error);
1917
+ const mapped = error instanceof OllamaStreamError && error.partialContent === "" ? new OllamaStreamError(error.message, {
1918
+ partialContent: extractPartialContent(accumulated),
1919
+ cause: error
1920
+ }) : mapError(error);
1553
1921
  this.rejectFinal(mapped);
1554
1922
  yield { type: "error", data: { error: mapped } };
1555
1923
  } finally {
@@ -1571,7 +1939,17 @@ function aggregateChat(accumulated, chunk) {
1571
1939
  role: message.role || accumulated.message.role,
1572
1940
  content: accumulated.message.content + (message.content ?? ""),
1573
1941
  thinking: message.thinking !== void 0 ? (accumulated.message.thinking ?? "") + message.thinking : accumulated.message.thinking,
1574
- tool_calls: message.tool_calls?.length ? [...accumulated.message.tool_calls ?? [], ...message.tool_calls] : accumulated.message.tool_calls
1942
+ // Defensive tool-call merging: instead of blindly appending
1943
+ // every chunk's tool_calls array (which would produce
1944
+ // duplicates if Ollama ever starts streaming them
1945
+ // incrementally), merge entries that refer to the same logical
1946
+ // call (matched by id or array position). For the documented
1947
+ // native behavior (tool_calls arrive complete in one chunk)
1948
+ // this is a no-op — the accumulator appends exactly once.
1949
+ //
1950
+ // See src/tools/tool-call-accumulator.ts for the full merge
1951
+ // semantics and the rationale.
1952
+ tool_calls: message.tool_calls?.length || accumulated.message.tool_calls?.length ? mergeToolCallArrays(accumulated.message.tool_calls, message.tool_calls) : void 0
1575
1953
  },
1576
1954
  model: chunk.model,
1577
1955
  done: chunk.done,
@@ -1780,12 +2158,24 @@ function createTimeoutSignal(timeoutMs, userSignal) {
1780
2158
  return { signal: controller.signal, cancel };
1781
2159
  }
1782
2160
 
2161
+ // src/keep-alive.ts
2162
+ var KEEP_ALIVE_UNLOAD = 0;
2163
+ var KEEP_ALIVE_INDEFINITE = -1;
2164
+ function normalizeKeepAlive(keepAlive) {
2165
+ if (keepAlive === void 0) return void 0;
2166
+ if (keepAlive === "unload") return KEEP_ALIVE_UNLOAD;
2167
+ if (keepAlive === "indefinite") return KEEP_ALIVE_INDEFINITE;
2168
+ return keepAlive;
2169
+ }
2170
+ function isKeepAliveSugar(value) {
2171
+ return value === "unload" || value === "indefinite";
2172
+ }
2173
+
1783
2174
  // src/models-client.ts
1784
2175
  var ModelsClient = class {
1785
2176
  constructor(runner) {
1786
2177
  this.runner = runner;
1787
2178
  }
1788
- runner;
1789
2179
  list() {
1790
2180
  return this.runner((http) => listAvailableModels(http), { singleEndpoint: true });
1791
2181
  }
@@ -1892,6 +2282,76 @@ var ModelsClient = class {
1892
2282
  { singleEndpoint: true }
1893
2283
  );
1894
2284
  }
2285
+ /**
2286
+ * Immediately unloads `model` from VRAM/RAM by issuing an empty
2287
+ * `/api/generate` request with `keep_alive: 0`. The server
2288
+ * finalizes any in-flight generation, releases the model's
2289
+ * weights from GPU memory, and frees the slot for subsequent
2290
+ * pipelines (e.g. swap a vision encoder out before loading an
2291
+ * LLM).
2292
+ *
2293
+ * This is the ergonomic equivalent of:
2294
+ *
2295
+ * ```ts
2296
+ * client.generate({ model, prompt: '', keep_alive: 0 });
2297
+ * ```
2298
+ *
2299
+ * Use {@link pin} for the opposite lifecycle — pinning a model
2300
+ * indefinitely in VRAM for hot-loop inference.
2301
+ *
2302
+ * Resolves once the server acknowledges the unload. The promise
2303
+ * rejects on transport errors, model-not-found (404), or auth
2304
+ * failures — callers handling transient issues can retry via the
2305
+ * standard {@link withRetry} policy.
2306
+ *
2307
+ * See: https://github.com/ollama/ollama/blob/main/docs/faq.md
2308
+ * #how-do-i-keep-a-model-loaded-in-memory-or-make-it-unload-immediately
2309
+ */
2310
+ async unload(model) {
2311
+ await this.runner(
2312
+ (http, signal) => http.request({
2313
+ path: "/api/generate",
2314
+ body: { model, prompt: "", keep_alive: KEEP_ALIVE_UNLOAD, stream: false },
2315
+ signal
2316
+ }),
2317
+ { model, singleEndpoint: true }
2318
+ );
2319
+ }
2320
+ /**
2321
+ * Pre-loads and pins `model` into VRAM indefinitely by issuing an
2322
+ * empty `/api/generate` request with `keep_alive: -1`. The server
2323
+ * loads the model into GPU memory (paying the cold-load latency
2324
+ * once) and keeps it resident until either:
2325
+ *
2326
+ * - an explicit {@link unload} call is made,
2327
+ * - the server process is restarted, or
2328
+ * - the host runs out of VRAM and the server's own eviction
2329
+ * policy kicks in.
2330
+ *
2331
+ * This is the ergonomic equivalent of:
2332
+ *
2333
+ * ```ts
2334
+ * client.generate({ model, prompt: '', keep_alive: -1 });
2335
+ * ```
2336
+ *
2337
+ * Use {@link unload} to release the pinned slot when the hot loop
2338
+ * is done — leaving models pinned indefinitely exhausts VRAM and
2339
+ * starves subsequent pipelines.
2340
+ *
2341
+ * Resolves once the server acknowledges the pin (i.e. the model
2342
+ * is fully loaded). The promise rejects on transport errors,
2343
+ * model-not-found (404), or VRAM-exhaustion failures.
2344
+ */
2345
+ async pin(model) {
2346
+ await this.runner(
2347
+ (http, signal) => http.request({
2348
+ path: "/api/generate",
2349
+ body: { model, prompt: "", keep_alive: KEEP_ALIVE_INDEFINITE, stream: false },
2350
+ signal
2351
+ }),
2352
+ { model, singleEndpoint: true }
2353
+ );
2354
+ }
1895
2355
  async createBlob(digest, data) {
1896
2356
  await this.runner(
1897
2357
  (http, signal) => http.request({
@@ -1919,6 +2379,100 @@ var ModelsClient = class {
1919
2379
  throw err;
1920
2380
  }
1921
2381
  }
2382
+ /**
2383
+ * Computes a blob digest the way Ollama's content-addressed store expects:
2384
+ * SHA-256 over the raw bytes, formatted as `sha256:<64 lowercase hex>`.
2385
+ *
2386
+ * Uses the Web Crypto API (`crypto.subtle.digest`), which exists in every
2387
+ * supported runtime — Node.js ≥ 19 exposes it as a global (this SDK
2388
+ * requires Node ≥ 20), and browsers/edge runtimes have always had it.
2389
+ */
2390
+ async computeBlobDigest(data) {
2391
+ const subtle = globalThis.crypto?.subtle;
2392
+ if (subtle === void 0) {
2393
+ throw new OllamaClientError(
2394
+ "computeBlobDigest requires the Web Crypto API (crypto.subtle), unavailable in this runtime.",
2395
+ { code: "crypto_unavailable" }
2396
+ );
2397
+ }
2398
+ const digestBytes = await subtle.digest("SHA-256", data);
2399
+ const hex = Array.from(
2400
+ new Uint8Array(digestBytes),
2401
+ (byte) => byte.toString(16).padStart(2, "0")
2402
+ ).join("");
2403
+ return `sha256:${hex}`;
2404
+ }
2405
+ /**
2406
+ * Computes the SHA-256 digest of `data` and pushes it as a blob in one
2407
+ * step — the content-addressed upload Ollama's own tooling performs
2408
+ * (`curl -T file http://…/api/blobs/sha256:…`). Returns the digest, which
2409
+ * `/api/create`'s `files` field references.
2410
+ *
2411
+ * Pass `digest` to skip recomputation when the caller already knows it —
2412
+ * the server verifies the content against the digest and rejects mismatches.
2413
+ */
2414
+ async createBlobFromData(data, opts) {
2415
+ const digest = opts?.digest ?? await this.computeBlobDigest(data);
2416
+ const alreadyExisted = await this.checkBlob(digest);
2417
+ if (!alreadyExisted) {
2418
+ await this.createBlob(digest, data);
2419
+ }
2420
+ return { digest, alreadyExisted };
2421
+ }
2422
+ /**
2423
+ * Reads a local file and uploads it as a blob (Node.js only — dynamic
2424
+ * `node:fs` import, so browser bundles never pull it in). The file name
2425
+ * (basename) is what `/api/create`'s `files` map expects as the key, so
2426
+ * this returns it alongside the digest.
2427
+ */
2428
+ async createBlobFromFile(path, opts) {
2429
+ const nodeFsModule = "node:fs";
2430
+ const { promises: fs } = await import(nodeFsModule);
2431
+ const data = await fs.readFile(path);
2432
+ const result = await this.createBlobFromData(new Uint8Array(data), {
2433
+ ...opts
2434
+ });
2435
+ const fileName = path.split(/[\\/]/).pop() ?? path;
2436
+ return { ...result, fileName };
2437
+ }
2438
+ /**
2439
+ * One-shot custom-model publishing from raw GGUF weights, following the
2440
+ * protocol Ollama's API reference documents: push a blob for the GGUF
2441
+ * bytes, then `POST /api/create` with `files: { <fileName>: <digest> }`.
2442
+ * Split GGUFs are supported via `files` — pass every shard's path.
2443
+ *
2444
+ * ```ts
2445
+ * await client.models.createModelFromGguf('my-model', '/path/to/model.gguf', {
2446
+ * template: '{{ .Prompt }}',
2447
+ * });
2448
+ * ```
2449
+ *
2450
+ * @param model Name for the new model (e.g. `'my-model:latest'`).
2451
+ * @param gguf Path(s) to GGUF file(s) on the local filesystem.
2452
+ * @param opts Everything else `/api/create` accepts (`template`, `system`,
2453
+ * `parameters`, `license`, `quantize`, …) except `files` and `from`.
2454
+ */
2455
+ async createModelFromGguf(model, gguf, opts) {
2456
+ const paths = typeof gguf === "string" ? [gguf] : gguf;
2457
+ if (paths.length === 0) {
2458
+ throw new OllamaClientError("createModelFromGguf: at least one GGUF path is required.", {
2459
+ code: "invalid_request"
2460
+ });
2461
+ }
2462
+ const files = {};
2463
+ for (const path of paths) {
2464
+ const uploaded = await this.createBlobFromFile(path, { signal: opts?.signal });
2465
+ files[uploaded.fileName] = uploaded.digest;
2466
+ }
2467
+ const {
2468
+ model: _model,
2469
+ files: _files,
2470
+ from: _from,
2471
+ stream: _stream,
2472
+ ...rest
2473
+ } = opts ?? {};
2474
+ return this.create({ ...rest, model, files, stream: false });
2475
+ }
1922
2476
  };
1923
2477
 
1924
2478
  // src/integrations/openai.ts
@@ -1945,7 +2499,6 @@ var OpenAIChatCompletionStream = class {
1945
2499
  }
1946
2500
  }
1947
2501
  }
1948
- source;
1949
2502
  finalResultPromise;
1950
2503
  resolveFinal;
1951
2504
  rejectFinal;
@@ -2089,7 +2642,6 @@ var OpenAICompletionStream = class {
2089
2642
  }
2090
2643
  }
2091
2644
  }
2092
- source;
2093
2645
  finalResultPromise;
2094
2646
  resolveFinal;
2095
2647
  rejectFinal;
@@ -2170,7 +2722,6 @@ var OpenAIResponsesStream = class {
2170
2722
  }
2171
2723
  }
2172
2724
  }
2173
- source;
2174
2725
  finalResultPromise;
2175
2726
  resolveFinal;
2176
2727
  rejectFinal;
@@ -2395,8 +2946,6 @@ var OpenAICompatClient = class {
2395
2946
  this.http = http;
2396
2947
  this.runner = runner;
2397
2948
  }
2398
- http;
2399
- runner;
2400
2949
  request(operation, model, signal, holdUntil) {
2401
2950
  if (this.runner) {
2402
2951
  return this.runner(
@@ -2616,7 +3165,6 @@ var AnthropicMessagesStream = class {
2616
3165
  }
2617
3166
  }
2618
3167
  }
2619
- source;
2620
3168
  finalResultPromise;
2621
3169
  resolveFinal;
2622
3170
  rejectFinal;
@@ -2731,8 +3279,6 @@ var AnthropicCompatClient = class {
2731
3279
  this.http = http;
2732
3280
  this.runner = runner;
2733
3281
  }
2734
- http;
2735
- runner;
2736
3282
  request(operation, model, signal, holdUntil) {
2737
3283
  if (this.runner) {
2738
3284
  return this.runner(
@@ -2778,6 +3324,32 @@ var AnthropicCompatClient = class {
2778
3324
  return this.createMessage(request, signal);
2779
3325
  }
2780
3326
  };
3327
+ var IncludedBalanceSchema = z.object({
3328
+ balance_usd: z.number(),
3329
+ allowance_usd: z.number(),
3330
+ period: z.object({
3331
+ from: z.string(),
3332
+ until: z.string()
3333
+ })
3334
+ });
3335
+ var LegacyBalanceLimitSchema = z.object({
3336
+ remaining_percent: z.number().min(0).max(100),
3337
+ resets_at: z.string()
3338
+ });
3339
+
3340
+ // src/generated/models/LegacyIncludedBalance.schema.ts
3341
+ var LegacyIncludedBalanceSchema = z.object({
3342
+ session: LegacyBalanceLimitSchema,
3343
+ weekly: LegacyBalanceLimitSchema
3344
+ });
3345
+
3346
+ // src/generated/models/BalanceResponse.schema.ts
3347
+ var BalanceResponseSchema = z.object({
3348
+ included: z.union([IncludedBalanceSchema, LegacyIncludedBalanceSchema]),
3349
+ purchased: z.object({
3350
+ balance_usd: z.number()
3351
+ })
3352
+ });
2781
3353
  var ToolCallSchema = z.object({
2782
3354
  function: z.object({
2783
3355
  name: z.string(),
@@ -2881,6 +3453,7 @@ var CreateRequestSchema = z.object({
2881
3453
  quantize: z.string().optional(),
2882
3454
  draft_quantize: z.string().optional(),
2883
3455
  requires: z.string().optional(),
3456
+ capabilities: z.array(z.string()).optional(),
2884
3457
  stream: z.boolean().optional()
2885
3458
  });
2886
3459
  var DeleteRequestSchema = z.object({
@@ -2943,7 +3516,7 @@ var ModelSummarySchema = z.object({
2943
3516
  details: z.object({
2944
3517
  format: z.string().optional(),
2945
3518
  family: z.string().optional(),
2946
- families: z.array(z.string()).optional(),
3519
+ families: z.union([z.array(z.string()), z.null()]).optional(),
2947
3520
  parameter_size: z.string().optional(),
2948
3521
  quantization_level: z.string().optional()
2949
3522
  }).optional()
@@ -3078,6 +3651,34 @@ var SystemOneResponseSchema = z.object({
3078
3651
  output_tokens: z.number().min(0)
3079
3652
  })
3080
3653
  });
3654
+ var UsageMetricsSchema = z.object({
3655
+ request_count: z.number().min(0),
3656
+ usage_usd: z.number().optional(),
3657
+ input_tokens: z.number().optional(),
3658
+ cached_input_tokens: z.number().optional(),
3659
+ output_tokens: z.number().optional()
3660
+ });
3661
+
3662
+ // src/generated/models/UsageBucket.schema.ts
3663
+ var UsageBucketSchema = z.intersection(
3664
+ UsageMetricsSchema,
3665
+ z.object({
3666
+ from: z.string(),
3667
+ until: z.string(),
3668
+ partial: z.boolean().optional()
3669
+ })
3670
+ );
3671
+
3672
+ // src/generated/models/UsageResponse.schema.ts
3673
+ var UsageResponseSchema = z.object({
3674
+ range: z.enum(["24h", "7d", "30d"]),
3675
+ scope: z.enum(["self", "team"]),
3676
+ granularity: z.enum(["hour", "day"]),
3677
+ from: z.string(),
3678
+ until: z.string(),
3679
+ totals: UsageMetricsSchema,
3680
+ buckets: z.array(UsageBucketSchema)
3681
+ });
3081
3682
  var VersionResponseSchema = z.object({
3082
3683
  version: z.string().optional()
3083
3684
  });
@@ -3120,6 +3721,7 @@ var requestSchemas = {
3120
3721
  webSearch: WebSearchRequestSchema
3121
3722
  };
3122
3723
  var responseSchemas = {
3724
+ balance: BalanceResponseSchema,
3123
3725
  chat: ChatResponseSchema,
3124
3726
  create: StatusResponseSchema,
3125
3727
  embed: EmbedResponseSchema,
@@ -3130,6 +3732,7 @@ var responseSchemas = {
3130
3732
  show: ShowResponseSchema,
3131
3733
  systemOne: SystemOneResponseSchema,
3132
3734
  tags: ListResponseSchema,
3735
+ usage: UsageResponseSchema,
3133
3736
  version: VersionResponseSchema,
3134
3737
  webFetch: WebFetchResponseSchema,
3135
3738
  webSearch: WebSearchResponseSchema
@@ -3180,7 +3783,6 @@ var OllamaRuntime = class {
3180
3783
  constructor(options) {
3181
3784
  this.options = options;
3182
3785
  }
3183
- options;
3184
3786
  /**
3185
3787
  * Cached promise of the Ollama server version, populated lazily the first
3186
3788
  * time a version-gated operation is invoked. `undefined` until then.
@@ -3257,11 +3859,19 @@ var OllamaRuntime = class {
3257
3859
  }
3258
3860
  const streaming = shouldStream(req);
3259
3861
  const resolvedPath = resolvePathParams(req.operation.path, req.pathParams);
3862
+ const pathWithQuery = applyQueryParams(resolvedPath, req.queryParams);
3260
3863
  const model = req.model ?? extractModelFromBody(body);
3864
+ const headers = {};
3865
+ if (req.headerParams) {
3866
+ for (const [key, value] of Object.entries(req.headerParams)) {
3867
+ headers[key] = value;
3868
+ }
3869
+ }
3261
3870
  const httpReq = {
3262
- path: resolvedPath,
3871
+ path: pathWithQuery,
3263
3872
  method: req.operation.method,
3264
3873
  ...body !== void 0 ? { body } : {},
3874
+ ...Object.keys(headers).length > 0 ? { headers } : {},
3265
3875
  ...req.signal !== void 0 ? { signal: req.signal } : {},
3266
3876
  ...streaming ? { raw: true } : {},
3267
3877
  ...model !== void 0 ? { model } : {}
@@ -3402,6 +4012,589 @@ function extractModelFromBody(body) {
3402
4012
  const model = obj.model;
3403
4013
  return typeof model === "string" ? model : void 0;
3404
4014
  }
4015
+ function applyQueryParams(path, queryParams) {
4016
+ if (!queryParams || Object.keys(queryParams).length === 0) return path;
4017
+ const pairs = Object.entries(queryParams).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
4018
+ return `${path}?${pairs}`;
4019
+ }
4020
+
4021
+ // src/responses.ts
4022
+ var adapterResponseSequence = 0;
4023
+ function toNativeTools(tools) {
4024
+ if (tools === void 0 || tools.length === 0) return void 0;
4025
+ return tools.map((tool) => ({
4026
+ type: "function",
4027
+ function: {
4028
+ name: tool.name,
4029
+ description: tool.description ?? "",
4030
+ parameters: tool.parameters ?? { type: "object", properties: {} }
4031
+ }
4032
+ }));
4033
+ }
4034
+ function mapNativeUsage(usage) {
4035
+ if (usage === void 0) return void 0;
4036
+ const input = usage.input_tokens ?? 0;
4037
+ const output = usage.output_tokens ?? 0;
4038
+ return { input_tokens: input, output_tokens: output, total_tokens: input + output };
4039
+ }
4040
+ function parseToolCallArguments(raw) {
4041
+ if (raw === void 0 || raw === null || raw === "") return {};
4042
+ try {
4043
+ const parsed = JSON.parse(raw);
4044
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
4045
+ } catch {
4046
+ return { _raw: raw };
4047
+ }
4048
+ }
4049
+ function mapNativeResponse(native) {
4050
+ const textParts = [];
4051
+ const thinkingParts = [];
4052
+ const toolCalls = [];
4053
+ for (const item of native.output ?? []) {
4054
+ if (item.type === "message") {
4055
+ for (const part of item.content ?? []) {
4056
+ if (part.type === "output_text") textParts.push(part.text ?? "");
4057
+ else if (part.type === "refusal") textParts.push(part.refusal ?? "");
4058
+ }
4059
+ } else if (item.type === "reasoning") {
4060
+ if (item.text !== void 0 && item.text !== "") thinkingParts.push(item.text);
4061
+ } else if (item.type === "function_call") {
4062
+ toolCalls.push({
4063
+ ...item.id !== void 0 ? { id: item.id } : {},
4064
+ function: { name: item.name, arguments: parseToolCallArguments(item.arguments) }
4065
+ });
4066
+ }
4067
+ }
4068
+ return {
4069
+ id: native.id ?? `resp_${Date.now().toString(36)}`,
4070
+ model: native.model,
4071
+ created_at: new Date((native.created_at ?? 0) * 1e3).toISOString(),
4072
+ output_text: textParts.join(""),
4073
+ ...thinkingParts.length > 0 ? { thinking: thinkingParts.join("\n") } : {},
4074
+ ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {},
4075
+ ...mapNativeUsage(native.usage) !== void 0 ? { usage: mapNativeUsage(native.usage) } : {}
4076
+ };
4077
+ }
4078
+ function mapChatUsage(res) {
4079
+ const asChat = res;
4080
+ const asStream = res;
4081
+ const input = asChat.prompt_eval_count ?? asStream.usage?.promptTokens ?? asStream.raw?.prompt_eval_count ?? 0;
4082
+ const output = asChat.eval_count ?? asStream.usage?.completionTokens ?? asStream.raw?.eval_count ?? 0;
4083
+ if (input === 0 && output === 0) return void 0;
4084
+ return { input_tokens: input, output_tokens: output, total_tokens: input + output };
4085
+ }
4086
+ function mapChatResponse(res) {
4087
+ adapterResponseSequence += 1;
4088
+ return {
4089
+ id: `resp_${Date.now().toString(36)}${adapterResponseSequence.toString(36)}`,
4090
+ model: res.model,
4091
+ created_at: res.created_at,
4092
+ output_text: res.message.content,
4093
+ ...res.message.thinking !== void 0 ? { thinking: res.message.thinking } : {},
4094
+ ...res.message.tool_calls !== void 0 && res.message.tool_calls.length > 0 ? { tool_calls: res.message.tool_calls } : {},
4095
+ ...mapChatUsage(res) !== void 0 ? { usage: mapChatUsage(res) } : {},
4096
+ transport: "chat-adapter"
4097
+ };
4098
+ }
4099
+ var ResponsesModule = class {
4100
+ constructor(runner) {
4101
+ this.runner = runner;
4102
+ }
4103
+ /** The request body sent to the native endpoint (also reused for streaming). */
4104
+ nativeBody(req) {
4105
+ return {
4106
+ model: req.model,
4107
+ input: req.input,
4108
+ ...req.instructions !== void 0 ? { instructions: req.instructions } : {},
4109
+ ...req.tools !== void 0 && req.tools.length > 0 ? { tools: toNativeTools(req.tools) } : {},
4110
+ ...req.temperature !== void 0 ? { temperature: req.temperature } : {},
4111
+ ...req.top_p !== void 0 ? { top_p: req.top_p } : {},
4112
+ ...req.max_output_tokens !== void 0 ? { max_output_tokens: req.max_output_tokens } : {},
4113
+ ...req.reasoning_effort !== void 0 ? { reasoning: { effort: req.reasoning_effort } } : {},
4114
+ ...req.think !== void 0 ? { think: req.think } : {}
4115
+ };
4116
+ }
4117
+ /**
4118
+ * Runs one Responses request, preferring the native `POST /v1/responses`
4119
+ * endpoint and transparently re-issuing via `/api/chat` when the server
4120
+ * answers `404` (pre-v0.13.3 Ollama without the compatibility endpoint).
4121
+ */
4122
+ async create(req) {
4123
+ const body = this.nativeBody(req);
4124
+ try {
4125
+ const native = await this.runner(
4126
+ (http, signal) => http.request({
4127
+ path: "/v1/responses",
4128
+ body: { ...body, stream: false },
4129
+ signal
4130
+ }),
4131
+ { model: req.model, signal: req.signal, timeoutMs: req.timeoutMs }
4132
+ );
4133
+ return { ...mapNativeResponse(native), transport: "native" };
4134
+ } catch (err) {
4135
+ if (!(err instanceof OllamaNotFoundError)) throw err;
4136
+ }
4137
+ return this.createViaChatAdapter(req);
4138
+ }
4139
+ /** Convenience: `create()` returning only the `output_text` string. */
4140
+ async createText(req) {
4141
+ const res = await this.create(req);
4142
+ return res.output_text;
4143
+ }
4144
+ async createViaChatAdapter(req) {
4145
+ const messages = [
4146
+ ...req.instructions !== void 0 ? [{ role: "system", content: req.instructions }] : [],
4147
+ { role: "user", content: req.input }
4148
+ ];
4149
+ const chatRes = await this.runner(
4150
+ (http, signal) => http.request({
4151
+ path: "/api/chat",
4152
+ body: {
4153
+ model: req.model,
4154
+ messages,
4155
+ ...req.tools !== void 0 && req.tools.length > 0 ? { tools: toNativeTools(req.tools) } : {},
4156
+ options: {
4157
+ ...req.temperature !== void 0 ? { temperature: req.temperature } : {},
4158
+ ...req.top_p !== void 0 ? { top_p: req.top_p } : {},
4159
+ ...req.max_output_tokens !== void 0 ? { num_predict: req.max_output_tokens } : {}
4160
+ },
4161
+ ...req.think !== void 0 ? { think: req.think } : {},
4162
+ stream: false
4163
+ },
4164
+ signal
4165
+ }),
4166
+ { model: req.model, signal: req.signal, timeoutMs: req.timeoutMs }
4167
+ );
4168
+ return mapChatResponse(chatRes);
4169
+ }
4170
+ /**
4171
+ * Streams a Responses request as simplified
4172
+ * `{type: 'text_delta' | 'thinking_delta' | 'done'}` events. Dual-mode like
4173
+ * {@link create}: native SSE from `/v1/responses` when available, otherwise
4174
+ * the normalized `/api/chat` token stream. Tool calls are not delta'd —
4175
+ * collect them from the final `done` event's `response.tool_calls`.
4176
+ *
4177
+ * This is a lazy async generator: nothing is sent until the first
4178
+ * iteration, matching `for await (... of client.responses.stream(req))`
4179
+ * intuition. The generator is also cancellable through `req.signal`.
4180
+ */
4181
+ async *stream(req) {
4182
+ const body = this.nativeBody(req);
4183
+ let nativeStream;
4184
+ try {
4185
+ nativeStream = await this.runner(
4186
+ async (http, signal) => {
4187
+ const source = await http.requestSseStream({
4188
+ path: "/v1/responses",
4189
+ body: { ...body, stream: true },
4190
+ signal
4191
+ });
4192
+ return new OpenAIResponsesStream(source, signal);
4193
+ },
4194
+ {
4195
+ model: req.model,
4196
+ signal: req.signal,
4197
+ timeoutMs: req.timeoutMs,
4198
+ holdUntil: (s) => s.finalResult
4199
+ }
4200
+ );
4201
+ } catch (err) {
4202
+ if (!(err instanceof OllamaNotFoundError)) throw err;
4203
+ }
4204
+ if (nativeStream !== void 0) {
4205
+ for await (const event of nativeStream) {
4206
+ const type = event.type;
4207
+ if (type === "response.output_text.delta") {
4208
+ yield { type: "text_delta", delta: event.delta };
4209
+ } else if (type === "response.reasoning_text.delta") {
4210
+ yield { type: "thinking_delta", delta: event.delta };
4211
+ }
4212
+ }
4213
+ yield {
4214
+ type: "done",
4215
+ response: { ...mapNativeResponse(await nativeStream.finalResult), transport: "native" }
4216
+ };
4217
+ return;
4218
+ }
4219
+ const chatStream = await this.runner(
4220
+ async (http, signal) => {
4221
+ const source = await http.requestStream({
4222
+ path: "/api/chat",
4223
+ body: {
4224
+ model: req.model,
4225
+ messages: [
4226
+ ...req.instructions !== void 0 ? [{ role: "system", content: req.instructions }] : [],
4227
+ { role: "user", content: req.input }
4228
+ ],
4229
+ ...req.tools !== void 0 && req.tools.length > 0 ? { tools: toNativeTools(req.tools) } : {},
4230
+ options: {
4231
+ ...req.temperature !== void 0 ? { temperature: req.temperature } : {},
4232
+ ...req.top_p !== void 0 ? { top_p: req.top_p } : {},
4233
+ ...req.max_output_tokens !== void 0 ? { num_predict: req.max_output_tokens } : {}
4234
+ },
4235
+ ...req.think !== void 0 ? { think: req.think } : {},
4236
+ stream: true
4237
+ },
4238
+ signal
4239
+ });
4240
+ return normalizeChatStream(source, signal);
4241
+ },
4242
+ {
4243
+ model: req.model,
4244
+ signal: req.signal,
4245
+ timeoutMs: req.timeoutMs,
4246
+ holdUntil: (s) => s.finalResult
4247
+ }
4248
+ );
4249
+ for await (const event of chatStream) {
4250
+ if (event.type === "token") {
4251
+ yield { type: "text_delta", delta: event.data.delta };
4252
+ } else if (event.type === "thinking") {
4253
+ yield { type: "thinking_delta", delta: event.data.delta };
4254
+ }
4255
+ }
4256
+ const result = await chatStream.finalResult;
4257
+ yield {
4258
+ type: "done",
4259
+ response: {
4260
+ id: `resp_${Date.now().toString(36)}${(++adapterResponseSequence).toString(36)}`,
4261
+ model: result.model,
4262
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
4263
+ output_text: result.message.content,
4264
+ ...result.message.thinking !== void 0 ? { thinking: result.message.thinking } : {},
4265
+ ...result.message.tool_calls !== void 0 && result.message.tool_calls.length > 0 ? { tool_calls: result.message.tool_calls } : {},
4266
+ ...mapChatUsage(result) !== void 0 ? { usage: mapChatUsage(result) } : {},
4267
+ transport: "chat-adapter"
4268
+ }
4269
+ };
4270
+ }
4271
+ };
4272
+
4273
+ // src/conversation.ts
4274
+ function extractTurnStats(res) {
4275
+ const cachedTokens = res.prompt_eval_cached_count ?? 0;
4276
+ const evaluatedTokens = res.prompt_eval_count ?? 0;
4277
+ const total = cachedTokens + evaluatedTokens;
4278
+ return {
4279
+ cachedTokens,
4280
+ evaluatedTokens,
4281
+ hitRate: total > 0 ? cachedTokens / total : 0
4282
+ };
4283
+ }
4284
+ var ConversationSession = class {
4285
+ constructor(client, config) {
4286
+ this.client = client;
4287
+ this.config = config;
4288
+ if (config.systemPrompt !== void 0 && config.systemPrompt !== "") {
4289
+ this.history.push(Object.freeze({ role: "system", content: config.systemPrompt }));
4290
+ }
4291
+ }
4292
+ /** Ordered, append-only history. Message objects are frozen on push. */
4293
+ history = [];
4294
+ /** Mutable tallies behind the read-only {@link cacheStats} view. */
4295
+ tallies = {
4296
+ turns: 0,
4297
+ cachedTokens: 0,
4298
+ evaluatedTokens: 0
4299
+ };
4300
+ /** The model name this session talks to. */
4301
+ get model() {
4302
+ return this.config.model;
4303
+ }
4304
+ /** Number of completed turns. */
4305
+ get turnCount() {
4306
+ return this.tallies.turns;
4307
+ }
4308
+ /** Cumulative KV-cache statistics across completed turns. */
4309
+ get cacheStats() {
4310
+ const total = this.tallies.cachedTokens + this.tallies.evaluatedTokens;
4311
+ return {
4312
+ turns: this.tallies.turns,
4313
+ cachedTokens: this.tallies.cachedTokens,
4314
+ evaluatedTokens: this.tallies.evaluatedTokens,
4315
+ totalPromptTokens: total,
4316
+ hitRate: total > 0 ? this.tallies.cachedTokens / total : 0
4317
+ };
4318
+ }
4319
+ /**
4320
+ * Sends the user's message and returns the assistant's reply text —
4321
+ * the ergonomic path. Use {@link sendTurn} when you also need the
4322
+ * per-turn cache statistics or the raw response.
4323
+ */
4324
+ async send(content, overrides) {
4325
+ const turn = await this.sendTurn(content, overrides);
4326
+ return turn.content;
4327
+ }
4328
+ /**
4329
+ * Sends the user's message and returns the full turn record: reply text,
4330
+ * per-turn KV-cache statistics, and the raw `/api/chat` response.
4331
+ */
4332
+ async sendTurn(content, overrides) {
4333
+ this.history.push(Object.freeze({ role: "user", content }));
4334
+ const response = await this.client.chat({
4335
+ model: this.config.model,
4336
+ messages: [...this.history],
4337
+ ...this.config.tools !== void 0 || overrides?.tools !== void 0 ? { tools: overrides?.tools ?? this.config.tools } : {},
4338
+ ...this.config.format !== void 0 || overrides?.format !== void 0 ? { format: overrides?.format ?? this.config.format } : {},
4339
+ options: { ...this.config.options, ...overrides?.options },
4340
+ ...this.config.think !== void 0 || overrides?.think !== void 0 ? { think: overrides?.think ?? this.config.think } : {},
4341
+ ...this.config.keep_alive !== void 0 || overrides?.keep_alive !== void 0 ? { keep_alive: overrides?.keep_alive ?? this.config.keep_alive } : {},
4342
+ ...overrides?.signal !== void 0 ? { signal: overrides.signal } : {},
4343
+ ...overrides?.timeoutMs !== void 0 ? { timeoutMs: overrides.timeoutMs } : {},
4344
+ stream: false
4345
+ });
4346
+ this.history.push(Object.freeze({ ...response.message }));
4347
+ const cache = extractTurnStats(response);
4348
+ this.tallies.turns += 1;
4349
+ this.tallies.cachedTokens += cache.cachedTokens;
4350
+ this.tallies.evaluatedTokens += cache.evaluatedTokens;
4351
+ return { content: response.message.content, cache, response };
4352
+ }
4353
+ /**
4354
+ * A defensive copy of the session history — mutating the returned array or
4355
+ * its frozen messages has no effect on the session.
4356
+ */
4357
+ getMessages() {
4358
+ return this.history.map((message) => message);
4359
+ }
4360
+ /**
4361
+ * Clears the conversation back to its initial state (system prompt, zero
4362
+ * turns, zeroed cache statistics). The next turn starts with a cold cache
4363
+ * by definition — there is nothing upstream of it to reuse.
4364
+ */
4365
+ reset() {
4366
+ this.history.length = 0;
4367
+ if (this.config.systemPrompt !== void 0 && this.config.systemPrompt !== "") {
4368
+ this.history.push(
4369
+ Object.freeze({ role: "system", content: this.config.systemPrompt })
4370
+ );
4371
+ }
4372
+ this.tallies.turns = 0;
4373
+ this.tallies.cachedTokens = 0;
4374
+ this.tallies.evaluatedTokens = 0;
4375
+ }
4376
+ };
4377
+
4378
+ // src/context-safety.ts
4379
+ var DEFAULT_CONTEXT_WARNING_THRESHOLD = 0.9;
4380
+ var IMAGE_TOKEN_ESTIMATE = 300;
4381
+ var CHARS_PER_TOKEN = 4;
4382
+ function isCjkCodePoint(code) {
4383
+ return code >= 19968 && code <= 40959 || // CJK Unified Ideographs
4384
+ code >= 13312 && code <= 19903 || // CJK Extension A
4385
+ code >= 12352 && code <= 12543 || // Hiragana + Katakana
4386
+ code >= 44032 && code <= 55215 || // Hangul syllables
4387
+ code >= 63744 && code <= 64255 || // CJK Compatibility Ideographs
4388
+ code >= 65280 && code <= 65519;
4389
+ }
4390
+ function estimateTokens(text) {
4391
+ if (text.length === 0) return 0;
4392
+ let cjk = 0;
4393
+ let other = 0;
4394
+ for (const char of text) {
4395
+ if (isCjkCodePoint(char.codePointAt(0) ?? 0)) cjk += 1;
4396
+ else other += 1;
4397
+ }
4398
+ return Math.ceil(cjk + other / CHARS_PER_TOKEN);
4399
+ }
4400
+ var MESSAGE_OVERHEAD_TOKENS = 4;
4401
+ function estimateMessageTokens(message) {
4402
+ let total = MESSAGE_OVERHEAD_TOKENS + estimateTokens(message.content ?? "");
4403
+ if (message.thinking !== void 0) total += estimateTokens(message.thinking);
4404
+ if (message.tool_calls !== void 0) {
4405
+ for (const call of message.tool_calls) {
4406
+ total += estimateTokens(call.function.name);
4407
+ total += estimateTokens(JSON.stringify(call.function.arguments ?? {}));
4408
+ }
4409
+ }
4410
+ if (message.images !== void 0) total += message.images.length * IMAGE_TOKEN_ESTIMATE;
4411
+ return total;
4412
+ }
4413
+ function estimateChatRequestTokens(req) {
4414
+ let total = 0;
4415
+ for (const message of req.messages) total += estimateMessageTokens(message);
4416
+ if (req.tools !== void 0) {
4417
+ for (const tool of req.tools) {
4418
+ total += estimateTokens(tool.function.description ?? "");
4419
+ total += estimateTokens(JSON.stringify(tool.function.parameters ?? {}));
4420
+ }
4421
+ }
4422
+ return total;
4423
+ }
4424
+ function estimateGenerateRequestTokens(req) {
4425
+ let total = estimateTokens(req.prompt ?? "");
4426
+ if (req.system !== void 0) total += estimateTokens(req.system);
4427
+ if (req.template !== void 0) total += estimateTokens(req.template);
4428
+ if (req.images !== void 0) total += req.images.length * IMAGE_TOKEN_ESTIMATE;
4429
+ return total;
4430
+ }
4431
+ function checkChatContext(req, options = {}) {
4432
+ return check(estimateChatRequestTokens(req), req.options?.num_ctx, options);
4433
+ }
4434
+ function checkGenerateContext(req, options = {}) {
4435
+ return check(estimateGenerateRequestTokens(req), req.options?.num_ctx, options);
4436
+ }
4437
+ function check(estimatedPromptTokens, requestNumCtx, options) {
4438
+ const threshold = options.threshold ?? DEFAULT_CONTEXT_WARNING_THRESHOLD;
4439
+ if (requestNumCtx !== void 0) {
4440
+ return {
4441
+ estimatedPromptTokens,
4442
+ effectiveContextLength: requestNumCtx,
4443
+ windowSource: "request",
4444
+ exceedsThreshold: estimatedPromptTokens > requestNumCtx * threshold
4445
+ };
4446
+ }
4447
+ if (options.defaultContextLength !== void 0) {
4448
+ return {
4449
+ estimatedPromptTokens,
4450
+ effectiveContextLength: options.defaultContextLength,
4451
+ windowSource: "client-default",
4452
+ exceedsThreshold: estimatedPromptTokens > options.defaultContextLength * threshold
4453
+ };
4454
+ }
4455
+ return {
4456
+ estimatedPromptTokens,
4457
+ effectiveContextLength: OLLAMA_FALLBACK_CONTEXT_LENGTH,
4458
+ windowSource: "server-fallback",
4459
+ exceedsThreshold: estimatedPromptTokens > OLLAMA_FALLBACK_CONTEXT_LENGTH
4460
+ };
4461
+ }
4462
+ var OLLAMA_FALLBACK_CONTEXT_LENGTH = 2048;
4463
+ function contextWarningMessage(check2, kind) {
4464
+ const where = `OllamaClient.${kind}()`;
4465
+ const estimate = `~${check2.estimatedPromptTokens} tokens`;
4466
+ if (check2.windowSource === "request") {
4467
+ return `${where}: estimated prompt size ${estimate} is close to or beyond the request's context window (num_ctx=${check2.effectiveContextLength}). The server will likely silently truncate the top of the conversation \u2014 the model loses earlier turns with no error. Raise \`options.num_ctx\` or shorten the conversation. Estimates are heuristic (\xB120\u201330%); see \`estimateTokens\`.`;
4468
+ }
4469
+ if (check2.windowSource === "client-default") {
4470
+ return `${where}: estimated prompt size ${estimate} is close to or beyond the client's \`defaultContextLength\` (${check2.effectiveContextLength}, injected as \`num_ctx\`). The server will likely silently truncate the top of the conversation \u2014 the model loses earlier turns with no error. Raise \`defaultContextLength\`, set \`options.num_ctx\` per request, or shorten the conversation. Estimates are heuristic (\xB120\u201330%); see \`estimateTokens\`.`;
4471
+ }
4472
+ return `${where}: estimated prompt size ${estimate} outright exceeds Ollama's conservative server-default context window (${OLLAMA_FALLBACK_CONTEXT_LENGTH}\u20134096 tokens when \`num_ctx\` is unset \u2014 model-dependent). Unless the model's default is larger, the server will silently truncate the top of the conversation \u2014 the model loses earlier turns with no error. Set \`options.num_ctx\` (or the client's \`defaultContextLength\`) to an explicit window to make truncation visible and controllable. Estimates are heuristic (\xB120\u201330%); see \`estimateTokens\`.`;
4473
+ }
4474
+
4475
+ // src/embed-batch.ts
4476
+ var DEFAULT_EMBED_BATCH_SIZE = 32;
4477
+ var DEFAULT_EMBED_CONCURRENCY = 3;
4478
+ function findOversizedEmbedInputs(input, contextLength) {
4479
+ const oversized = [];
4480
+ for (let index = 0; index < input.length; index++) {
4481
+ const text = input[index];
4482
+ if (text === void 0) continue;
4483
+ const estimate = estimateTokens(text);
4484
+ if (estimate > contextLength) {
4485
+ oversized.push({ index, estimate });
4486
+ }
4487
+ }
4488
+ return oversized;
4489
+ }
4490
+ function embedBatchOverflowMessage(oversized, totalInputs, contextLength, truncateFalse) {
4491
+ const shown = oversized.slice(0, 5).map((entry) => `#${entry.index} (~${entry.estimate} tokens)`);
4492
+ const elided = oversized.length > 5 ? `, +${oversized.length - 5} more` : "";
4493
+ const outcome = truncateFalse ? "the server will reject them (`truncate: false`)" : "Ollama will silently truncate them, producing degraded vectors";
4494
+ return `embedBatch: ${oversized.length} of ${totalInputs} input strings exceed the ${contextLength}-token context window (${shown.join(", ")}${elided}). ${outcome}. Raise \`options.num_ctx\`, pre-split long inputs, or set \`truncate: true\` explicitly to acknowledge truncation.`;
4495
+ }
4496
+ function assertPositiveInteger(value, field) {
4497
+ if (!Number.isInteger(value) || value < 1) {
4498
+ throw new OllamaClientError(
4499
+ `embedBatch: \`${field}\` must be a positive integer (got ${value}).`,
4500
+ { code: "invalid_request" }
4501
+ );
4502
+ }
4503
+ }
4504
+ async function batchEmbed(client, options) {
4505
+ const { model, input } = options;
4506
+ const batchSize = options.batchSize ?? DEFAULT_EMBED_BATCH_SIZE;
4507
+ const concurrency = options.concurrency ?? DEFAULT_EMBED_CONCURRENCY;
4508
+ assertPositiveInteger(batchSize, "batchSize");
4509
+ assertPositiveInteger(concurrency, "concurrency");
4510
+ if (input.length === 0) {
4511
+ return { model, embeddings: [], batchCount: 0 };
4512
+ }
4513
+ const batches = [];
4514
+ for (let start = 0; start < input.length; start += batchSize) {
4515
+ batches.push(input.slice(start, start + batchSize));
4516
+ }
4517
+ const embeddings = new Array(input.length);
4518
+ const controller = new AbortController();
4519
+ const propagateExternalAbort = () => {
4520
+ controller.abort(options.signal?.reason);
4521
+ };
4522
+ if (options.signal) {
4523
+ if (options.signal.aborted) {
4524
+ propagateExternalAbort();
4525
+ } else {
4526
+ options.signal.addEventListener("abort", propagateExternalAbort, { once: true });
4527
+ }
4528
+ }
4529
+ let nextBatch = 0;
4530
+ let completedBatches = 0;
4531
+ let failure;
4532
+ const worker = async () => {
4533
+ for (; ; ) {
4534
+ if (controller.signal.aborted) {
4535
+ throw failure?.error ?? new OllamaAbortError("Embed batch aborted");
4536
+ }
4537
+ const batchIndex = nextBatch;
4538
+ nextBatch++;
4539
+ if (batchIndex >= batches.length) return;
4540
+ const batch = batches[batchIndex];
4541
+ if (!batch) return;
4542
+ try {
4543
+ const request = {
4544
+ model,
4545
+ input: batch,
4546
+ ...options.truncate !== void 0 ? { truncate: options.truncate } : {},
4547
+ ...options.dimensions !== void 0 ? { dimensions: options.dimensions } : {},
4548
+ ...options.keep_alive !== void 0 ? { keep_alive: options.keep_alive } : {},
4549
+ ...options.options !== void 0 ? { options: options.options } : {},
4550
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
4551
+ signal: controller.signal
4552
+ };
4553
+ const response = await client.embed(request);
4554
+ if (response.embeddings.length !== batch.length) {
4555
+ throw new OllamaClientError(
4556
+ `embedBatch: server returned ${response.embeddings.length} embeddings for a batch of ${batch.length} inputs (batch ${batchIndex}).`,
4557
+ { code: "invalid_response" }
4558
+ );
4559
+ }
4560
+ const offset = batchIndex * batchSize;
4561
+ for (let j = 0; j < batch.length; j++) {
4562
+ embeddings[offset + j] = response.embeddings[j];
4563
+ }
4564
+ completedBatches++;
4565
+ options.onBatchComplete?.(completedBatches, batches.length);
4566
+ } catch (error) {
4567
+ if (!failure) {
4568
+ failure = { error };
4569
+ controller.abort(error);
4570
+ throw error;
4571
+ }
4572
+ throw failure.error;
4573
+ }
4574
+ }
4575
+ };
4576
+ try {
4577
+ const workerCount = Math.min(concurrency, batches.length);
4578
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
4579
+ } finally {
4580
+ if (options.signal) {
4581
+ options.signal.removeEventListener("abort", propagateExternalAbort);
4582
+ }
4583
+ }
4584
+ for (let i = 0; i < embeddings.length; i++) {
4585
+ if (embeddings[i] === void 0) {
4586
+ throw new OllamaClientError(
4587
+ `embedBatch: internal invariant violated \u2014 no embedding produced for input #${i}.`,
4588
+ { code: "invalid_response" }
4589
+ );
4590
+ }
4591
+ }
4592
+ return {
4593
+ model,
4594
+ embeddings,
4595
+ batchCount: batches.length
4596
+ };
4597
+ }
3405
4598
 
3406
4599
  // src/generated/api/operations.ts
3407
4600
  var blobsOp = {
@@ -3613,7 +4806,6 @@ var NativeApi = class {
3613
4806
  constructor(runtime) {
3614
4807
  this.runtime = runtime;
3615
4808
  }
3616
- runtime;
3617
4809
  blobs(digest, options) {
3618
4810
  return this.runtime.invoke({
3619
4811
  operation: blobsOp,
@@ -3716,7 +4908,6 @@ var FailoverHttpClient = class {
3716
4908
  this.client = client;
3717
4909
  this.baseUrl = baseUrl;
3718
4910
  }
3719
- client;
3720
4911
  /**
3721
4912
  * The first candidate endpoint's base URL. Used for inferRuntimeMode()
3722
4913
  * checks (local vs cloud). The actual request routing picks the best
@@ -3923,14 +5114,29 @@ var OllamaClient = class {
3923
5114
  * `OLLAMA_API_KEY`), independent of whether `config.endpoints` was used instead.
3924
5115
  */
3925
5116
  cloudApiKey;
5117
+ /** Configured `defaultContextLength` — injected as `num_ctx` when a request omits it. */
5118
+ defaultContextLength;
5119
+ /** Configured `contextWarningThreshold` (default 0.9). */
5120
+ contextWarningThreshold;
5121
+ /** Configured overflow policy — `'warn'` (default) or `'throw'`. */
5122
+ onContextOverflow;
5123
+ /**
5124
+ * Controllers for every in-flight request and active stream managed by this
5125
+ * client — see {@link OllamaClient.destroy} and {@link createRequestScope}.
5126
+ * Entries live for exactly as long as the underlying work does: until the
5127
+ * response settles (requests) or the stream's `finalResult` settles
5128
+ * (streams, via `executeWithFailover`'s `holdUntil` release path).
5129
+ */
5130
+ activeRequests = /* @__PURE__ */ new Set();
3926
5131
  constructor(config = {}) {
3927
- const resolvedApiKey = resolveApiKey(config.apiKey);
3928
- this.cloudApiKey = resolvedApiKey;
5132
+ this.cloudApiKey = resolveApiKey(config.apiKey);
5133
+ const defaultBaseUrl = resolveBaseUrl(config.baseUrl);
5134
+ const resolvedApiKey = resolveEndpointApiKey(config.apiKey, defaultBaseUrl);
3929
5135
  const credentialEndpoints = resolveCredentialEndpoints(config);
3930
5136
  const endpoints = config.endpoints ?? (credentialEndpoints.length > 0 ? [] : [
3931
5137
  {
3932
5138
  name: "default",
3933
- baseUrl: resolveBaseUrl(config.baseUrl),
5139
+ baseUrl: defaultBaseUrl,
3934
5140
  ...resolvedApiKey !== void 0 ? { apiKey: resolvedApiKey } : {},
3935
5141
  ...config.headers !== void 0 ? { headers: config.headers } : {}
3936
5142
  }
@@ -3946,6 +5152,9 @@ var OllamaClient = class {
3946
5152
  this.middleware = config.middleware;
3947
5153
  this.onLifecycleEvent = config.onLifecycleEvent;
3948
5154
  this.retryConfig = typeof config.retries === "number" ? { ...DEFAULT_RETRY_CONFIG, maxRetries: config.retries } : { ...DEFAULT_RETRY_CONFIG, ...config.retries };
5155
+ this.defaultContextLength = config.defaultContextLength;
5156
+ this.contextWarningThreshold = config.contextWarningThreshold;
5157
+ this.onContextOverflow = config.onContextOverflow ?? "warn";
3949
5158
  this.models = new ModelsClient((op, opts) => this.executeWithFailover(op, opts));
3950
5159
  }
3951
5160
  get modelsClient() {
@@ -4035,6 +5244,87 @@ var OllamaClient = class {
4035
5244
  return this._decision;
4036
5245
  }
4037
5246
  _decision;
5247
+ /**
5248
+ * Ergonomic OpenAI Responses API bridge (`client.responses.create()` /
5249
+ * `.stream()`). Dual-mode transport: prefers the native `POST
5250
+ * /v1/responses` endpoint (Ollama ≥ v0.13.3) and transparently re-issues via
5251
+ * `/api/chat` when the server answers 404, so migrating OpenAI code works
5252
+ * unchanged against older servers. See `src/responses.ts`.
5253
+ */
5254
+ get responses() {
5255
+ if (this._responses === void 0) {
5256
+ this._responses = new ResponsesModule((op, opts) => this.executeWithFailover(op, opts));
5257
+ }
5258
+ return this._responses;
5259
+ }
5260
+ _responses;
5261
+ session(modelOrOptions, systemPrompt) {
5262
+ const options = typeof modelOrOptions === "string" ? {
5263
+ model: modelOrOptions,
5264
+ ...systemPrompt !== void 0 ? { systemPrompt } : {}
5265
+ } : modelOrOptions;
5266
+ return new ConversationSession(this, options);
5267
+ }
5268
+ /**
5269
+ * Context-window pre-flight enforcement — see `src/context-safety.ts` and
5270
+ * the `defaultContextLength` / `onContextOverflow` config docs. Logs a
5271
+ * warning (default) or throws client-side when the estimated prompt size
5272
+ * is close to / beyond the effective context window.
5273
+ */
5274
+ enforceContextSafety(kind, check2) {
5275
+ if (!check2.exceedsThreshold) return;
5276
+ const message = contextWarningMessage(check2, kind);
5277
+ if (this.onContextOverflow === "throw") {
5278
+ throw new OllamaClientError(message, { code: "context_overflow" });
5279
+ }
5280
+ this.logger.warn(message);
5281
+ }
5282
+ /**
5283
+ * Returns `options` with `num_ctx` injected from the client's
5284
+ * `defaultContextLength` when the request didn't set one — making the
5285
+ * effective context window explicit on the wire instead of relying on
5286
+ * Ollama's silently-truncating server default. Returns the original
5287
+ * reference when there's nothing to inject (the common case).
5288
+ */
5289
+ injectDefaultContextLength(options) {
5290
+ if (options?.num_ctx !== void 0 || this.defaultContextLength === void 0) {
5291
+ return options;
5292
+ }
5293
+ return { ...options, num_ctx: this.defaultContextLength };
5294
+ }
5295
+ /**
5296
+ * Creates the per-request abort scope registered with {@link destroy}:
5297
+ * a controller that fires when the caller's `signal` aborts OR when
5298
+ * `destroy()` runs, whichever comes first. `dispose()` removes the
5299
+ * registration when the work it guards settles — for plain requests when
5300
+ * the response settles, for streams when `finalResult` settles (the same
5301
+ * moment `executeWithFailover`'s `holdUntil` releases the endpoint slot),
5302
+ * so a destroy() can tear down a stream mid-consumption but never outlives
5303
+ * one that already finished.
5304
+ */
5305
+ createRequestScope(userSignal) {
5306
+ const controller = new AbortController();
5307
+ const propagateUserAbort = () => {
5308
+ controller.abort(userSignal?.reason);
5309
+ };
5310
+ let detach;
5311
+ if (userSignal) {
5312
+ if (userSignal.aborted) {
5313
+ propagateUserAbort();
5314
+ } else {
5315
+ userSignal.addEventListener("abort", propagateUserAbort, { once: true });
5316
+ detach = () => userSignal.removeEventListener("abort", propagateUserAbort);
5317
+ }
5318
+ }
5319
+ this.activeRequests.add(controller);
5320
+ return {
5321
+ signal: controller.signal,
5322
+ dispose: () => {
5323
+ detach?.();
5324
+ this.activeRequests.delete(controller);
5325
+ }
5326
+ };
5327
+ }
4038
5328
  /**
4039
5329
  * Fail-fast guard for `format` (structured output) requests: throws before any network
4040
5330
  * call if the candidate endpoint is inferred as Ollama Cloud, which does not currently
@@ -4051,7 +5341,8 @@ var OllamaClient = class {
4051
5341
  }
4052
5342
  }
4053
5343
  async executeWithFailover(operation, options) {
4054
- const timeout = createTimeoutSignal(options?.timeoutMs ?? this.timeoutMs, options?.signal);
5344
+ const scope = this.createRequestScope(options?.signal);
5345
+ const timeout = createTimeoutSignal(options?.timeoutMs ?? this.timeoutMs, scope.signal);
4055
5346
  const requestId = createLogicalRequestId();
4056
5347
  let deferTimeoutCancel = false;
4057
5348
  try {
@@ -4130,6 +5421,7 @@ var OllamaClient = class {
4130
5421
  if (holdPromise) {
4131
5422
  void holdPromise.catch(() => void 0).finally(() => {
4132
5423
  timeout.cancel();
5424
+ scope.dispose();
4133
5425
  this.registry.release(endpoint.name);
4134
5426
  });
4135
5427
  } else {
@@ -4140,11 +5432,14 @@ var OllamaClient = class {
4140
5432
  throw lastError ?? new Error("No healthy Ollama endpoints available");
4141
5433
  }
4142
5434
  } finally {
4143
- if (!deferTimeoutCancel) timeout.cancel();
5435
+ if (!deferTimeoutCancel) {
5436
+ timeout.cancel();
5437
+ scope.dispose();
5438
+ }
4144
5439
  }
4145
5440
  }
4146
5441
  async chat(req) {
4147
- const encodedMessages = await withEncodedMessageImages(req.messages);
5442
+ const encodedMessages = await withEncodedMessageImages(req.messages, req.signal);
4148
5443
  const messages = encodedMessages.map((message) => {
4149
5444
  if (message.role === "tool" && message.tool_call_id !== void 0) {
4150
5445
  const { tool_call_id: _toolCallId, ...nativeMessage } = message;
@@ -4152,6 +5447,14 @@ var OllamaClient = class {
4152
5447
  }
4153
5448
  return message;
4154
5449
  });
5450
+ this.enforceContextSafety(
5451
+ "chat",
5452
+ checkChatContext(req, {
5453
+ defaultContextLength: this.defaultContextLength,
5454
+ threshold: this.contextWarningThreshold
5455
+ })
5456
+ );
5457
+ const options = this.injectDefaultContextLength(req.options);
4155
5458
  if (req.stream) {
4156
5459
  return this.executeWithFailover(
4157
5460
  async (http, signal) => {
@@ -4159,7 +5462,7 @@ var OllamaClient = class {
4159
5462
  this.assertStructuredOutputSupported(http.baseUrl, req.model);
4160
5463
  const stream = await http.requestStream({
4161
5464
  path: "/api/chat",
4162
- body: { ...req, messages, stream: true },
5465
+ body: { ...req, options, messages, stream: true },
4163
5466
  signal
4164
5467
  });
4165
5468
  return normalizeChatStream(stream, signal);
@@ -4188,7 +5491,7 @@ var OllamaClient = class {
4188
5491
  this.assertStructuredOutputSupported(http.baseUrl, req.model);
4189
5492
  return http.request({
4190
5493
  path: "/api/chat",
4191
- body: { ...req, messages, stream: false },
5494
+ body: { ...req, options, messages, stream: false },
4192
5495
  signal
4193
5496
  });
4194
5497
  }, req);
@@ -4215,7 +5518,15 @@ var OllamaClient = class {
4215
5518
  return parseStructuredOutput(res.message.content, schema);
4216
5519
  }
4217
5520
  async generate(req) {
4218
- const encodedReq = await withEncodedImages(req);
5521
+ const encodedReq = await withEncodedImages(req, req.signal);
5522
+ this.enforceContextSafety(
5523
+ "generate",
5524
+ checkGenerateContext(req, {
5525
+ defaultContextLength: this.defaultContextLength,
5526
+ threshold: this.contextWarningThreshold
5527
+ })
5528
+ );
5529
+ const options = this.injectDefaultContextLength(req.options);
4219
5530
  if (encodedReq.stream) {
4220
5531
  return this.executeWithFailover(
4221
5532
  async (http, signal) => {
@@ -4223,7 +5534,7 @@ var OllamaClient = class {
4223
5534
  this.assertStructuredOutputSupported(http.baseUrl, encodedReq.model);
4224
5535
  const stream = await http.requestStream({
4225
5536
  path: "/api/generate",
4226
- body: { ...encodedReq, stream: true },
5537
+ body: { ...encodedReq, options, stream: true },
4227
5538
  signal
4228
5539
  });
4229
5540
  return normalizeGenerateStream(stream, signal);
@@ -4249,7 +5560,7 @@ var OllamaClient = class {
4249
5560
  this.assertStructuredOutputSupported(http.baseUrl, encodedReq.model);
4250
5561
  return http.request({
4251
5562
  path: "/api/generate",
4252
- body: { ...encodedReq, stream: false },
5563
+ body: { ...encodedReq, options, stream: false },
4253
5564
  signal
4254
5565
  });
4255
5566
  }, encodedReq);
@@ -4284,6 +5595,58 @@ var OllamaClient = class {
4284
5595
  const res = await this.embed({ model, input });
4285
5596
  return res.embeddings;
4286
5597
  }
5598
+ /**
5599
+ * Batch-constrained, order-preserving embedding generation for large
5600
+ * corpora — the safe building block for RAG/vector-index ingestion at
5601
+ * scale. Splits `input` into `batchSize` slices and embeds them through
5602
+ * {@link embed} with at most `concurrency` batches in flight (defaults:
5603
+ * 32 × 3), so `Promise.all`-style floods that OOM local daemons and
5604
+ * saturate `OLLAMA_MAX_QUEUE` can't happen by accident. Fail-fast: the
5605
+ * first batch error aborts every sibling in-flight batch and rejects the
5606
+ * whole operation with the original error.
5607
+ *
5608
+ * Before any request is sent, `defaultContextLength` is injected as
5609
+ * `num_ctx` (same as chat/generate) and each input string's token estimate
5610
+ * is checked against the resolved window: Ollama silently truncates
5611
+ * oversized embedding inputs by default, so the client warns (or throws,
5612
+ * under `onContextOverflow: 'throw'`) with the offending indexes — see
5613
+ * `src/embed-batch.ts` for the exact trip point (no margin: an embedding
5614
+ * prompt IS the whole input).
5615
+ *
5616
+ * ```ts
5617
+ * const { embeddings, batchCount } = await client.embedBatch({
5618
+ * model: 'nomic-embed-text:latest',
5619
+ * input: corpus, // e.g. 50k chunks
5620
+ * batchSize: 32, // inputs per /api/embed request
5621
+ * concurrency: 3, // batches in flight
5622
+ * onBatchComplete: (done, total) => progress.log(`${done}/${total}`),
5623
+ * });
5624
+ * // embeddings[i] always corresponds to corpus[i]
5625
+ * ```
5626
+ */
5627
+ async embedBatch(options) {
5628
+ const resolvedOptions = this.injectDefaultContextLength(options.options);
5629
+ const contextLength = resolvedOptions?.num_ctx;
5630
+ if (contextLength !== void 0) {
5631
+ const oversized = findOversizedEmbedInputs(options.input, contextLength);
5632
+ if (oversized.length > 0) {
5633
+ const message = embedBatchOverflowMessage(
5634
+ oversized,
5635
+ options.input.length,
5636
+ contextLength,
5637
+ options.truncate === false
5638
+ );
5639
+ if (this.onContextOverflow === "throw") {
5640
+ throw new OllamaClientError(message, { code: "context_overflow" });
5641
+ }
5642
+ this.logger.warn(message);
5643
+ }
5644
+ }
5645
+ return batchEmbed(
5646
+ this,
5647
+ resolvedOptions === options.options ? options : { ...options, options: resolvedOptions }
5648
+ );
5649
+ }
4287
5650
  /**
4288
5651
  * @deprecated Ollama's `/api/embeddings` endpoint has been superseded by `/api/embed`
4289
5652
  * (exposed here as {@link OllamaClient.embed}), which additionally supports batch
@@ -4307,6 +5670,18 @@ var OllamaClient = class {
4307
5670
  version = () => this.models.version();
4308
5671
  createBlob = (digest, data) => this.models.createBlob(digest, data);
4309
5672
  checkBlob = (digest) => this.models.checkBlob(digest);
5673
+ /**
5674
+ * Immediately unloads `model` from VRAM. Convenience alias for
5675
+ * {@link ModelsClient.unload} — see that method for the full
5676
+ * lifecycle contract.
5677
+ */
5678
+ unloadModel = (model) => this.models.unload(model);
5679
+ /**
5680
+ * Pre-loads and pins `model` into VRAM indefinitely. Convenience
5681
+ * alias for {@link ModelsClient.pin} — see that method for the
5682
+ * full lifecycle contract.
5683
+ */
5684
+ pinModel = (model) => this.models.pin(model);
4310
5685
  // --- System One (Wave 13) ---
4311
5686
  /**
4312
5687
  * Ollama's System One decision layer (`POST /v1/systemone`) — evaluates
@@ -4376,6 +5751,63 @@ var OllamaClient = class {
4376
5751
  req
4377
5752
  );
4378
5753
  }
5754
+ /**
5755
+ * Ollama Cloud usage statistics (`GET https://ollama.com/api/usage`) — request counts,
5756
+ * USD spend, and token totals (including cached input tokens) for cloud inference,
5757
+ * web search, and web fetch, bucketed by hour (`range: '24h'`) or day (`'7d'`/`'30d'`).
5758
+ *
5759
+ * See {@link OllamaClient.webSearch} for the cloud-endpoint/auth/timeout/retry
5760
+ * behavior, which applies identically here. Query parameters mirror the official
5761
+ * endpoint: `range` (`'24h' | '7d' | '30d'`, server default `'7d'`) and `scope`
5762
+ * (`'self' | 'team'`, server default `'self'`; team scope requires a team admin).
5763
+ * Omitted options are left unset so the server applies its own defaults.
5764
+ *
5765
+ * ```ts
5766
+ * const usage = await client.usage({ range: '24h' });
5767
+ * console.log(usage.totals.request_count, usage.totals.usage_usd);
5768
+ * for (const bucket of usage.buckets) {
5769
+ * if (bucket.partial) continue; // current hour, still in progress
5770
+ * // ...
5771
+ * }
5772
+ * ```
5773
+ */
5774
+ usage(req = {}) {
5775
+ const params = new URLSearchParams();
5776
+ if (req.range !== void 0) params.set("range", req.range);
5777
+ if (req.scope !== void 0) params.set("scope", req.scope);
5778
+ const query = params.toString();
5779
+ return this.executeCloudRequest(
5780
+ (http, signal) => http.request({
5781
+ path: query ? `/api/usage?${query}` : "/api/usage",
5782
+ method: "GET",
5783
+ signal
5784
+ }),
5785
+ req
5786
+ );
5787
+ }
5788
+ /**
5789
+ * Ollama Cloud balance (`GET https://ollama.com/api/balance`) — remaining included
5790
+ * and purchased usage credits. The included balance is either a plan-period credit
5791
+ * object (`balance_usd`/`allowance_usd`/`period`) or, on legacy plans, session/weekly
5792
+ * percentage limits; the shape is discriminated at runtime by the server response.
5793
+ * See {@link OllamaClient.webSearch} for the cloud-endpoint/auth/timeout/retry
5794
+ * behavior, which applies identically here.
5795
+ *
5796
+ * ```ts
5797
+ * const balance = await client.balance();
5798
+ * if ('balance_usd' in balance.included) {
5799
+ * console.log(balance.included.balance_usd, balance.purchased.balance_usd);
5800
+ * } else {
5801
+ * console.log(balance.included.session.remaining_percent, '% of session limit left');
5802
+ * }
5803
+ * ```
5804
+ */
5805
+ balance(req = {}) {
5806
+ return this.executeCloudRequest(
5807
+ (http, signal) => http.request({ path: "/api/balance", method: "GET", signal }),
5808
+ req
5809
+ );
5810
+ }
4379
5811
  /**
4380
5812
  * Runs `operation` against the fixed Ollama Cloud host with this client's default
4381
5813
  * timeout and retry policy — the same primitives `executeWithFailover` uses, minus the
@@ -4384,10 +5816,12 @@ var OllamaClient = class {
4384
5816
  async executeCloudRequest(operation, options) {
4385
5817
  var _stack = [];
4386
5818
  try {
4387
- const timeout = createTimeoutSignal(options.timeoutMs ?? this.timeoutMs, options.signal);
5819
+ const scope = this.createRequestScope(options.signal);
5820
+ const timeout = createTimeoutSignal(options.timeoutMs ?? this.timeoutMs, scope.signal);
4388
5821
  const _timeout = __using(_stack, {
4389
5822
  [Symbol.dispose]() {
4390
5823
  timeout.cancel();
5824
+ scope.dispose();
4391
5825
  }
4392
5826
  });
4393
5827
  const requestId = createLogicalRequestId();
@@ -4448,6 +5882,34 @@ var OllamaClient = class {
4448
5882
  endpointStatus() {
4449
5883
  return this.registry.status();
4450
5884
  }
5885
+ /**
5886
+ * Immediately aborts every in-flight request and active stream managed by
5887
+ * this client — the clean teardown path for short-lived processes, CLI
5888
+ * runners, and `node:worker_threads` tasks where dangling fetch bodies,
5889
+ * unconsumed stream readers, or queued capacity waiters would otherwise
5890
+ * keep the event loop (and thus the thread/process) alive.
5891
+ *
5892
+ * Every aborted operation rejects with an {@link OllamaAbortError}
5893
+ * (`code: 'aborted'`) carrying `reason` as its message — including requests
5894
+ * still queued behind `maxConcurrentPerEndpoint`, not just dispatched ones.
5895
+ * Streams reject their `finalResult` and release their endpoint slots.
5896
+ *
5897
+ * Idempotent: calling it again (or on a client with nothing in flight)
5898
+ * is a no-op that returns `0`. The client remains usable afterward —
5899
+ * `destroy()` is a drain, not a permanent disable; callers that want a
5900
+ * permanently-dead client should discard the instance.
5901
+ *
5902
+ * @returns the number of in-flight operations that were aborted.
5903
+ */
5904
+ destroy(reason = "Ollama client destroyed") {
5905
+ const controllers = [...this.activeRequests];
5906
+ const cause = new OllamaAbortError(reason);
5907
+ for (const controller of controllers) {
5908
+ controller.abort(cause);
5909
+ }
5910
+ this.activeRequests.clear();
5911
+ return controllers.length;
5912
+ }
4451
5913
  // --- Compatibility Adapters ---
4452
5914
  get openai() {
4453
5915
  const ep = this.registry.candidates()[0];
@@ -4479,6 +5941,91 @@ var OllamaClient = class {
4479
5941
  }
4480
5942
  };
4481
5943
 
5944
+ // src/transport/fetch-with-backoff.ts
5945
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 503]);
5946
+ var DEFAULT_FETCH_BACKOFF_CONFIG = {
5947
+ maxRetries: 3
5948
+ };
5949
+ function sleep2(ms, signal) {
5950
+ return new Promise((resolve, reject) => {
5951
+ if (signal?.aborted) {
5952
+ reject(
5953
+ Object.assign(new Error("fetchWithBackoff aborted during backoff sleep"), {
5954
+ name: "AbortError"
5955
+ })
5956
+ );
5957
+ return;
5958
+ }
5959
+ const timer = setTimeout(() => {
5960
+ signal?.removeEventListener("abort", onAbort);
5961
+ resolve();
5962
+ }, ms);
5963
+ const onAbort = () => {
5964
+ clearTimeout(timer);
5965
+ signal?.removeEventListener("abort", onAbort);
5966
+ reject(
5967
+ Object.assign(new Error("fetchWithBackoff aborted during backoff sleep"), {
5968
+ name: "AbortError"
5969
+ })
5970
+ );
5971
+ };
5972
+ signal?.addEventListener("abort", onAbort, { once: true });
5973
+ });
5974
+ }
5975
+ async function fetchWithBackoff(input, init, config = {}) {
5976
+ const fetchImpl = config.fetch ?? globalThis.fetch;
5977
+ if (typeof fetchImpl !== "function") {
5978
+ throw new Error(
5979
+ "fetchWithBackoff: no global fetch is available. Pass a fetch implementation via config.fetch, or run in Node.js >= 18 / a browser-like environment."
5980
+ );
5981
+ }
5982
+ const maxRetries = config.maxRetries ?? DEFAULT_FETCH_BACKOFF_CONFIG.maxRetries;
5983
+ const backoff = config.backoff ?? DEFAULT_BACKOFF;
5984
+ const signal = config.signal;
5985
+ let lastError;
5986
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
5987
+ if (signal?.aborted) {
5988
+ throw Object.assign(new Error("fetchWithBackoff aborted"), { name: "AbortError" });
5989
+ }
5990
+ try {
5991
+ const response = await fetchImpl(input, {
5992
+ ...init,
5993
+ ...signal !== void 0 ? { signal } : {}
5994
+ });
5995
+ if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < maxRetries) {
5996
+ const delayMs = calculateBackoff(attempt, backoff);
5997
+ config.onRetry?.(attempt, response.status, delayMs);
5998
+ await sleep2(delayMs, signal);
5999
+ continue;
6000
+ }
6001
+ return response;
6002
+ } catch (err) {
6003
+ lastError = err;
6004
+ if (signal?.aborted || err instanceof Error && err.name === "AbortError") {
6005
+ throw err;
6006
+ }
6007
+ if (attempt < maxRetries) {
6008
+ const delayMs = calculateBackoff(attempt, backoff);
6009
+ config.onRetry?.(attempt, 0, delayMs);
6010
+ await sleep2(delayMs, signal);
6011
+ continue;
6012
+ }
6013
+ }
6014
+ }
6015
+ throw lastError ?? new Error("fetchWithBackoff: exhausted retry attempts");
6016
+ }
6017
+
6018
+ // src/done-reason.ts
6019
+ var KNOWN_DONE_REASONS = /* @__PURE__ */ new Set([
6020
+ "stop",
6021
+ "length",
6022
+ "load",
6023
+ "unload"
6024
+ ]);
6025
+ function isKnownDoneReason(value) {
6026
+ return value !== void 0 && value !== null && KNOWN_DONE_REASONS.has(value);
6027
+ }
6028
+
4482
6029
  // src/streaming/adapters.ts
4483
6030
  function toTextStream(stream) {
4484
6031
  const iterator = stream[Symbol.asyncIterator]();
@@ -4934,9 +6481,9 @@ function validate(value, schema, path) {
4934
6481
  ["exclusiveMinimum", (n, b) => n > b, "must be greater than"],
4935
6482
  ["exclusiveMaximum", (n, b) => n < b, "must be less than"]
4936
6483
  ];
4937
- for (const [keyword, check, phrase] of numericRules) {
6484
+ for (const [keyword, check2, phrase] of numericRules) {
4938
6485
  const bound = s[keyword];
4939
- if (typeof bound === "number" && !check(value, bound)) issues.push({ path, message: label(path) + " " + phrase + " " + bound });
6486
+ if (typeof bound === "number" && !check2(value, bound)) issues.push({ path, message: label(path) + " " + phrase + " " + bound });
4940
6487
  }
4941
6488
  const multipleOf = s["multipleOf"];
4942
6489
  if (typeof multipleOf === "number" && multipleOf > 0) {
@@ -5197,8 +6744,6 @@ var McpBridge = class _McpBridge {
5197
6744
  this.client = client;
5198
6745
  this.options = options;
5199
6746
  }
5200
- client;
5201
- options;
5202
6747
  static toOllamaTools(tools, options = {}) {
5203
6748
  const prefix = options.namePrefix ?? "";
5204
6749
  return tools.map((tool) => ({
@@ -5495,6 +7040,13 @@ var Agent = class {
5495
7040
  }
5496
7041
  };
5497
7042
 
5498
- export { ATTR_GEN_AI_OPERATION_NAME, ATTR_GEN_AI_REQUEST_MODEL, ATTR_GEN_AI_RESPONSE_MODEL, ATTR_GEN_AI_SYSTEM, ATTR_GEN_AI_TOOL_CALL_ID, ATTR_GEN_AI_TOOL_NAME, ATTR_GEN_AI_USAGE_INPUT_TOKENS, ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_OLLAMA_AGENT_ITERATION, ATTR_OLLAMA_AGENT_MAX_ITERATIONS, ATTR_OLLAMA_ENDPOINT_ATTEMPT, ATTR_OLLAMA_ENDPOINT_NAME, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL, Agent, AnthropicCompatClient, AnthropicMessagesStream, DEFAULT_BACKOFF, DEFAULT_BASE_URL, DEFAULT_FAILOVER_CODES, DEFAULT_RETRY_CONFIG, DEFAULT_TIMEOUT_MS, EndpointRegistry, GEN_AI_SYSTEM_OLLAMA, HttpClient, McpBridge, ModelsClient, NANOS_PER_MS, NANOS_PER_SECOND, NOOP_LOGGER, OLLAMA_CLOUD_BASE_URL, OllamaAbortError, OllamaAgentMaxIterationsError, OllamaAgentMaxToolCallsError, OllamaAuthError, OllamaClient, OllamaClientError, OllamaGenericClientError, OllamaIncompatibleModelError, OllamaMcpError, OllamaModelRoutingError, OllamaNetworkError, OllamaNotFoundError, OllamaQuotaExceededError, OllamaRateLimitError, OllamaRequestTooLargeError, OllamaRequestValidationError, OllamaResponseValidationError, OllamaServerError, OllamaServerVersionUnknownError, OllamaSkillInvalidError, OllamaSkillNotFoundError, OllamaStream, OllamaTimeoutError, OllamaToolTimeoutError, OllamaToolValidationError, OllamaUnsupportedCapabilityError, OpenAIChatCompletionStream, OpenAICompatClient, OpenAICompletionStream, OpenAIResponsesStream, OpenAIResponsesStreamError, QuotaManager, ToolRegistry, addSpanEvent, applySkill, calculateBackoff, checkEndpointHealth, composeMiddleware, createConsoleLogger, createDecision, createOllamaCloudFreeTierQuota, createTimeoutSignal, defineTool, detectModelCapabilities, encodeImage, extractUsage, inferRuntimeMode, listAllMcpTools, listAvailableModels, loadMcpTools, mapError, normalizeChatStream, normalizeGenerateStream, normalizeProgressStream, parseFrontmatter, parseNdjsonStream, parseSseStream, parseStructuredOutput, registerMcpTools, resolveCredentialEndpoints, setSpanError, toDataStream, toResponse, toTextStream, withRetry, withSpan, zodToJsonSchema };
7043
+ // src/system-one.ts
7044
+ var MAX_SYSTEM_ONE_IMAGES_BYTES = 32 * 1024 * 1024;
7045
+ var MAX_SYSTEM_ONE_REQUEST_BYTES = 64 * 1024;
7046
+ function estimateSystemOneRequestBytes(request) {
7047
+ return new TextEncoder().encode(JSON.stringify(request)).byteLength;
7048
+ }
7049
+
7050
+ export { ATTR_GEN_AI_OPERATION_NAME, ATTR_GEN_AI_REQUEST_MODEL, ATTR_GEN_AI_RESPONSE_MODEL, ATTR_GEN_AI_SYSTEM, ATTR_GEN_AI_TOOL_CALL_ID, ATTR_GEN_AI_TOOL_NAME, ATTR_GEN_AI_USAGE_INPUT_TOKENS, ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_OLLAMA_AGENT_ITERATION, ATTR_OLLAMA_AGENT_MAX_ITERATIONS, ATTR_OLLAMA_ENDPOINT_ATTEMPT, ATTR_OLLAMA_ENDPOINT_NAME, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL, Agent, AnthropicCompatClient, AnthropicMessagesStream, ConversationSession, DEFAULT_BACKOFF, DEFAULT_BASE_URL, DEFAULT_CONTEXT_WARNING_THRESHOLD, DEFAULT_EMBED_BATCH_SIZE, DEFAULT_EMBED_CONCURRENCY, DEFAULT_FAILOVER_CODES, DEFAULT_FETCH_BACKOFF_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TIMEOUT_MS, EndpointRegistry, GEN_AI_SYSTEM_OLLAMA, HttpClient, IMAGE_TOKEN_ESTIMATE, KEEP_ALIVE_INDEFINITE, KEEP_ALIVE_UNLOAD, KNOWN_DONE_REASONS, MAX_SYSTEM_ONE_IMAGES_BYTES, MAX_SYSTEM_ONE_REQUEST_BYTES, McpBridge, ModelsClient, NANOS_PER_MS, NANOS_PER_SECOND, NOOP_LOGGER, OLLAMA_CLOUD_BASE_URL, OLLAMA_FALLBACK_CONTEXT_LENGTH, OllamaAbortError, OllamaAgentMaxIterationsError, OllamaAgentMaxToolCallsError, OllamaAuthError, OllamaBadGatewayError, OllamaClient, OllamaClientError, OllamaGenericClientError, OllamaIncompatibleModelError, OllamaMcpError, OllamaModelRoutingError, OllamaNetworkError, OllamaNotFoundError, OllamaOverloadedError, OllamaQuotaExceededError, OllamaRateLimitError, OllamaRequestTooLargeError, OllamaRequestValidationError, OllamaResponseValidationError, OllamaServerError, OllamaServerVersionUnknownError, OllamaSkillInvalidError, OllamaSkillNotFoundError, OllamaStream, OllamaStreamError, OllamaTimeoutError, OllamaToolTimeoutError, OllamaToolValidationError, OllamaUnsupportedCapabilityError, OpenAIChatCompletionStream, OpenAICompatClient, OpenAICompletionStream, OpenAIResponsesStream, OpenAIResponsesStreamError, QuotaManager, RETRYABLE_STATUS_CODES, ResponsesModule, ToolCallAccumulator, ToolRegistry, addSpanEvent, applySkill, batchEmbed, calculateBackoff, checkChatContext, checkEndpointHealth, checkGenerateContext, composeMiddleware, contextWarningMessage, createConsoleLogger, createDecision, createOllamaCloudFreeTierQuota, createTimeoutSignal, defineTool, detectModelCapabilities, embedBatchOverflowMessage, encodeImage, estimateChatRequestTokens, estimateGenerateRequestTokens, estimateSystemOneRequestBytes, estimateTokens, extractUsage, fetchWithBackoff, findOversizedEmbedInputs, imageStringNeedsResolution, inferRuntimeMode, isKeepAliveSugar, isKnownDoneReason, isSameToolCall, listAllMcpTools, listAvailableModels, loadMcpTools, mapError, mergeToolCall, mergeToolCallArgumentsString, mergeToolCallArrays, normalizeChatStream, normalizeGenerateStream, normalizeKeepAlive, normalizeProgressStream, parseFrontmatter, parseNdjsonStream, parseSseStream, parseStructuredOutput, registerMcpTools, resolveCredentialEndpoints, resolveImageInput, setSpanError, toDataStream, toResponse, toTextStream, withRetry, withSpan, zodToJsonSchema };
5499
7051
  //# sourceMappingURL=index.js.map
5500
7052
  //# sourceMappingURL=index.js.map