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