@nemesis-oss/ollama-sdk 1.8.0 → 1.9.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 +158 -0
- package/README.md +181 -10
- package/dist/{anthropic-B_UXplNF.d.ts → anthropic-5oYIRS6D.d.ts} +4 -2
- package/dist/{anthropic-CVDFy_a0.d.cts → anthropic-BIh45UXj.d.cts} +4 -2
- package/dist/{compose-DLStUfaF.d.ts → compose-DCVYMvOL.d.ts} +1 -1
- package/dist/{compose-DS-UWFx8.d.cts → compose-Dj6FdF2n.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 +1384 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1312 -14
- package/dist/index.d.ts +1312 -14
- package/dist/index.js +1352 -52
- 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-C3rfpkZw.d.ts} +69 -2
- package/dist/{runtime-DTmUeU6K.d.cts → runtime-D2W9TfCJ.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-DVcX_zMp.d.cts} +110 -10
- package/dist/{types-5PAjLoCp.d.ts → types-DVcX_zMp.d.ts} +110 -10
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -46,6 +46,47 @@ var __callDispose = (stack, error, hasError) => {
|
|
|
46
46
|
return next();
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
// src/transport/host.ts
|
|
50
|
+
var DEFAULT_PORT = "11434";
|
|
51
|
+
var SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
52
|
+
function normalizeBaseUrl(host) {
|
|
53
|
+
const trimmed = host.trim();
|
|
54
|
+
if (trimmed === "") return trimmed;
|
|
55
|
+
const implicitScheme = !SCHEME_RE.test(trimmed);
|
|
56
|
+
const withHost = trimmed.startsWith(":") ? `127.0.0.1${trimmed}` : trimmed;
|
|
57
|
+
const candidate = implicitScheme ? `http://${withHost}` : withHost;
|
|
58
|
+
let url;
|
|
59
|
+
try {
|
|
60
|
+
url = new URL(candidate);
|
|
61
|
+
} catch {
|
|
62
|
+
return trimmed.replace(/\/+$/, "");
|
|
63
|
+
}
|
|
64
|
+
let protocol = url.protocol;
|
|
65
|
+
let port = url.port;
|
|
66
|
+
if (implicitScheme) {
|
|
67
|
+
if (port === "443") {
|
|
68
|
+
protocol = "https:";
|
|
69
|
+
port = "";
|
|
70
|
+
} else if (port === "") {
|
|
71
|
+
port = DEFAULT_PORT;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const authority = `${url.hostname}${port !== "" ? `:${port}` : ""}`;
|
|
75
|
+
return `${protocol}//${authority}${url.pathname}`.replace(/\/+$/, "");
|
|
76
|
+
}
|
|
77
|
+
function isEnvApiKeyHost(baseUrl) {
|
|
78
|
+
let hostname;
|
|
79
|
+
try {
|
|
80
|
+
hostname = new URL(normalizeBaseUrl(baseUrl)).hostname.toLowerCase();
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (hostname === "ollama.com" || hostname.endsWith(".ollama.com")) return true;
|
|
85
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) return true;
|
|
86
|
+
if (hostname === "[::1]" || hostname === "::1" || hostname === "0.0.0.0") return true;
|
|
87
|
+
return /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
88
|
+
}
|
|
89
|
+
|
|
49
90
|
// src/config.ts
|
|
50
91
|
var DEFAULT_BASE_URL = "http://localhost:11434";
|
|
51
92
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
@@ -54,11 +95,16 @@ function readEnv(name) {
|
|
|
54
95
|
return typeof process !== "undefined" && process.env ? process.env[name] : void 0;
|
|
55
96
|
}
|
|
56
97
|
function resolveBaseUrl(configBaseUrl) {
|
|
57
|
-
return configBaseUrl ?? readEnv("OLLAMA_HOST") ?? DEFAULT_BASE_URL;
|
|
98
|
+
return normalizeBaseUrl(configBaseUrl ?? readEnv("OLLAMA_HOST") ?? DEFAULT_BASE_URL);
|
|
58
99
|
}
|
|
59
100
|
function resolveApiKey(configApiKey) {
|
|
60
101
|
return configApiKey ?? readEnv("OLLAMA_API_KEY");
|
|
61
102
|
}
|
|
103
|
+
function resolveEndpointApiKey(configApiKey, baseUrl) {
|
|
104
|
+
if (configApiKey !== void 0) return configApiKey;
|
|
105
|
+
const envKey = readEnv("OLLAMA_API_KEY");
|
|
106
|
+
return envKey !== void 0 && isEnvApiKeyHost(baseUrl) ? envKey : void 0;
|
|
107
|
+
}
|
|
62
108
|
function resolveCredentialEndpoints(config) {
|
|
63
109
|
if (config.credentials === void 0) return [];
|
|
64
110
|
const modelsByCredential = /* @__PURE__ */ new Map();
|
|
@@ -91,6 +137,18 @@ var DEFAULT_FAILOVER_CODES = [
|
|
|
91
137
|
"network_error",
|
|
92
138
|
"timeout",
|
|
93
139
|
"server_error",
|
|
140
|
+
// HTTP 503 — server queue saturated (OLLAMA_MAX_QUEUE exceeded).
|
|
141
|
+
// The new OllamaOverloadedError specialization is retryable: the
|
|
142
|
+
// queue will eventually drain, and a different candidate endpoint
|
|
143
|
+
// may have capacity right now. Listed separately from
|
|
144
|
+
// 'server_error' because OllamaOverloadedError extends
|
|
145
|
+
// OllamaClientError (NOT OllamaServerError) — see
|
|
146
|
+
// src/errors.ts for the rationale.
|
|
147
|
+
"overloaded",
|
|
148
|
+
// HTTP 502 — cloud model could not be reached by the Ollama Cloud
|
|
149
|
+
// proxy. Fail over to a different model/endpoint rather than
|
|
150
|
+
// retrying the same one.
|
|
151
|
+
"bad_gateway",
|
|
94
152
|
"rate_limited",
|
|
95
153
|
"auth_error",
|
|
96
154
|
// A candidate endpoint rejected the request pre-flight for a known-unsupported
|
|
@@ -159,6 +217,24 @@ var OllamaServerError = class extends OllamaClientError {
|
|
|
159
217
|
super(message, { ...options, code: "server_error", retryable: true });
|
|
160
218
|
}
|
|
161
219
|
};
|
|
220
|
+
var OllamaOverloadedError = class extends OllamaClientError {
|
|
221
|
+
constructor(message, options) {
|
|
222
|
+
super(message, { ...options, code: "overloaded", status: 503, retryable: true });
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
var OllamaBadGatewayError = class extends OllamaClientError {
|
|
226
|
+
constructor(message, options) {
|
|
227
|
+
super(message, { ...options, code: "bad_gateway", status: 502, retryable: true });
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var OllamaStreamError = class extends OllamaClientError {
|
|
231
|
+
/** Content accumulated before the in-band error frame, or '' if none. */
|
|
232
|
+
partialContent;
|
|
233
|
+
constructor(message, options) {
|
|
234
|
+
super(message, { ...options, code: "stream_error", retryable: false });
|
|
235
|
+
this.partialContent = options?.partialContent ?? "";
|
|
236
|
+
}
|
|
237
|
+
};
|
|
162
238
|
var OllamaAbortError = class extends OllamaClientError {
|
|
163
239
|
constructor(message, options) {
|
|
164
240
|
super(message, { ...options, code: "aborted", retryable: false });
|
|
@@ -308,6 +384,12 @@ function statusToError(status, message, options) {
|
|
|
308
384
|
if (status === 429) {
|
|
309
385
|
return new OllamaRateLimitError(message, { ...options, status });
|
|
310
386
|
}
|
|
387
|
+
if (status === 502) {
|
|
388
|
+
return new OllamaBadGatewayError(message, { ...options });
|
|
389
|
+
}
|
|
390
|
+
if (status === 503) {
|
|
391
|
+
return new OllamaOverloadedError(message, { ...options });
|
|
392
|
+
}
|
|
311
393
|
if (status >= 500) {
|
|
312
394
|
return new OllamaServerError(message, { ...options, status });
|
|
313
395
|
}
|
|
@@ -578,6 +660,75 @@ function composeMiddleware(middlewares, finalHandler) {
|
|
|
578
660
|
};
|
|
579
661
|
}
|
|
580
662
|
|
|
663
|
+
// src/vision.ts
|
|
664
|
+
var DATA_URI_PATTERN = /^data:image\/[a-z0-9.+-]+;base64,/i;
|
|
665
|
+
var IMAGE_FILE_EXTENSION_PATTERN = /\.(png|jpe?g|webp|gif|bmp|tiff?|avif)$/i;
|
|
666
|
+
function imageStringNeedsResolution(input) {
|
|
667
|
+
return DATA_URI_PATTERN.test(input) || input.startsWith("http://") || input.startsWith("https://") || IMAGE_FILE_EXTENSION_PATTERN.test(input);
|
|
668
|
+
}
|
|
669
|
+
function hasNodeFs() {
|
|
670
|
+
return typeof process !== "undefined" && process !== null && typeof process.versions?.node === "string";
|
|
671
|
+
}
|
|
672
|
+
var NODE_FS_MODULE = "node:fs";
|
|
673
|
+
async function readFileAsBase64(path) {
|
|
674
|
+
const { promises: fs } = await import(NODE_FS_MODULE);
|
|
675
|
+
const data = await fs.readFile(path);
|
|
676
|
+
return data.toString("base64");
|
|
677
|
+
}
|
|
678
|
+
async function fetchUrlAsBase64(url, signal) {
|
|
679
|
+
const res = await fetch(url, signal !== void 0 ? { signal } : void 0);
|
|
680
|
+
if (!res.ok) {
|
|
681
|
+
throw new OllamaClientError(
|
|
682
|
+
`Failed to fetch image from URL "${url}": HTTP ${res.status} ${res.statusText}.`,
|
|
683
|
+
{ code: "image_fetch_failed" }
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
687
|
+
return encodeBytesBase64(new Uint8Array(arrayBuffer));
|
|
688
|
+
}
|
|
689
|
+
function encodeBytesBase64(bytes) {
|
|
690
|
+
const bufferCtor = globalThis.Buffer;
|
|
691
|
+
if (bufferCtor !== void 0) return bufferCtor.from(bytes).toString("base64");
|
|
692
|
+
const CHUNK_SIZE = 32768;
|
|
693
|
+
let binary = "";
|
|
694
|
+
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
|
|
695
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_SIZE));
|
|
696
|
+
}
|
|
697
|
+
return btoa(binary);
|
|
698
|
+
}
|
|
699
|
+
async function resolveImageInput(input, signal) {
|
|
700
|
+
if (typeof input !== "string") {
|
|
701
|
+
return encodeBytesBase64(input instanceof Uint8Array ? input : new Uint8Array(input));
|
|
702
|
+
}
|
|
703
|
+
if (DATA_URI_PATTERN.test(input)) {
|
|
704
|
+
return input.replace(DATA_URI_PATTERN, "").trim();
|
|
705
|
+
}
|
|
706
|
+
if (input.startsWith("http://") || input.startsWith("https://")) {
|
|
707
|
+
return fetchUrlAsBase64(input, signal);
|
|
708
|
+
}
|
|
709
|
+
if (IMAGE_FILE_EXTENSION_PATTERN.test(input)) {
|
|
710
|
+
if (!hasNodeFs()) {
|
|
711
|
+
throw new OllamaClientError(
|
|
712
|
+
`Vision input "${input}" looks like a local file path, but this runtime has no Node.js \`fs\` module (browser/edge). Read the file into bytes yourself and pass the \`Uint8Array\`, or pass a base64 string / data URI / http(s) URL instead.`,
|
|
713
|
+
{ code: "vision_path_unsupported_runtime" }
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
try {
|
|
717
|
+
return await readFileAsBase64(input);
|
|
718
|
+
} catch {
|
|
719
|
+
return input;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return input.trim();
|
|
723
|
+
}
|
|
724
|
+
async function resolveImages(images, signal) {
|
|
725
|
+
if (images === void 0) return void 0;
|
|
726
|
+
if (images.every((image) => typeof image === "string" && !imageStringNeedsResolution(image))) {
|
|
727
|
+
return images;
|
|
728
|
+
}
|
|
729
|
+
return Promise.all(images.map((image) => resolveImageInput(image, signal)));
|
|
730
|
+
}
|
|
731
|
+
|
|
581
732
|
// src/utils.ts
|
|
582
733
|
async function encodeImage(image) {
|
|
583
734
|
if (typeof image === "string") return image;
|
|
@@ -590,18 +741,22 @@ async function encodeImage(image) {
|
|
|
590
741
|
}
|
|
591
742
|
return btoa(binary);
|
|
592
743
|
}
|
|
593
|
-
async function encodeImages(images) {
|
|
594
|
-
|
|
595
|
-
return Promise.all(images.map((image) => encodeImage(image)));
|
|
744
|
+
async function encodeImages(images, signal) {
|
|
745
|
+
return resolveImages(images, signal);
|
|
596
746
|
}
|
|
597
|
-
async function withEncodedImages(req) {
|
|
598
|
-
|
|
599
|
-
|
|
747
|
+
async function withEncodedImages(req, signal) {
|
|
748
|
+
const needsResolution = req.images?.some(
|
|
749
|
+
(image) => typeof image !== "string" || imageStringNeedsResolution(image)
|
|
750
|
+
);
|
|
751
|
+
if (!needsResolution) return req;
|
|
752
|
+
return { ...req, images: await encodeImages(req.images ?? [], signal) };
|
|
600
753
|
}
|
|
601
|
-
async function withEncodedMessageImages(messages) {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
754
|
+
async function withEncodedMessageImages(messages, signal) {
|
|
755
|
+
const needsResolution = messages.some(
|
|
756
|
+
(message) => message.images?.some((image) => typeof image !== "string" || imageStringNeedsResolution(image))
|
|
757
|
+
);
|
|
758
|
+
if (!needsResolution) return messages;
|
|
759
|
+
return Promise.all(messages.map((message) => withEncodedImages(message, signal)));
|
|
605
760
|
}
|
|
606
761
|
function disposableTimer(timer) {
|
|
607
762
|
return {
|
|
@@ -630,6 +785,12 @@ function disposableSpan(span) {
|
|
|
630
785
|
}
|
|
631
786
|
|
|
632
787
|
// src/streaming/ndjson.ts
|
|
788
|
+
function isInBandErrorFrame(value) {
|
|
789
|
+
if (typeof value !== "object" || value === null) return false;
|
|
790
|
+
const record = value;
|
|
791
|
+
if (typeof record.error !== "string" || record.error.length === 0) return false;
|
|
792
|
+
return !("model" in record) && !("response" in record) && !("message" in record) && !("status" in record) && !("done" in record);
|
|
793
|
+
}
|
|
633
794
|
async function* parseNdjsonStream(stream) {
|
|
634
795
|
var _stack = [];
|
|
635
796
|
try {
|
|
@@ -646,17 +807,34 @@ async function* parseNdjsonStream(stream) {
|
|
|
646
807
|
for (const line of lines) {
|
|
647
808
|
const trimmed = line.trim();
|
|
648
809
|
if (!trimmed) continue;
|
|
810
|
+
let json;
|
|
649
811
|
try {
|
|
650
|
-
|
|
812
|
+
json = JSON.parse(trimmed);
|
|
651
813
|
} catch (err) {
|
|
652
814
|
throw new OllamaGenericClientError(`Failed to parse stream JSON chunk: ${trimmed}`, {
|
|
653
815
|
cause: err
|
|
654
816
|
});
|
|
655
817
|
}
|
|
818
|
+
if (isInBandErrorFrame(json)) {
|
|
819
|
+
throw new OllamaStreamError(json.error);
|
|
820
|
+
}
|
|
821
|
+
yield json;
|
|
656
822
|
}
|
|
657
823
|
}
|
|
658
824
|
if (buffer.trim()) {
|
|
659
|
-
|
|
825
|
+
const trimmed = buffer.trim();
|
|
826
|
+
let json;
|
|
827
|
+
try {
|
|
828
|
+
json = JSON.parse(trimmed);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
throw new OllamaGenericClientError(`Failed to parse stream JSON chunk: ${trimmed}`, {
|
|
831
|
+
cause: err
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
if (isInBandErrorFrame(json)) {
|
|
835
|
+
throw new OllamaStreamError(json.error);
|
|
836
|
+
}
|
|
837
|
+
yield json;
|
|
660
838
|
}
|
|
661
839
|
} catch (_) {
|
|
662
840
|
var _error = _, _hasError = true;
|
|
@@ -867,7 +1045,7 @@ var HttpClient = class {
|
|
|
867
1045
|
onLifecycleEvent;
|
|
868
1046
|
requestId;
|
|
869
1047
|
constructor(options) {
|
|
870
|
-
this.baseUrl = options.baseUrl
|
|
1048
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
871
1049
|
this.apiKey = options.apiKey;
|
|
872
1050
|
this.defaultHeaders = options.headers ?? {};
|
|
873
1051
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
@@ -1256,7 +1434,7 @@ async function checkEndpointHealth(endpoint, fetchImpl, timeoutMs = 5e3, middlew
|
|
|
1256
1434
|
function inferRuntimeMode(baseUrl) {
|
|
1257
1435
|
try {
|
|
1258
1436
|
const url = new URL(baseUrl);
|
|
1259
|
-
const host = url.hostname.toLowerCase();
|
|
1437
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
1260
1438
|
if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.startsWith("192.168.") || host.startsWith("10.")) {
|
|
1261
1439
|
return "local";
|
|
1262
1440
|
}
|
|
@@ -1274,6 +1452,63 @@ function extractContextLength(modelInfo) {
|
|
|
1274
1452
|
}
|
|
1275
1453
|
return void 0;
|
|
1276
1454
|
}
|
|
1455
|
+
function extractModelFamily(showRes) {
|
|
1456
|
+
if (showRes.model_info) {
|
|
1457
|
+
for (const [key, value] of Object.entries(showRes.model_info)) {
|
|
1458
|
+
if (key.endsWith(".architecture") && typeof value === "string" && value.length > 0) {
|
|
1459
|
+
return value.toLowerCase();
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
if (showRes.details?.family && showRes.details.family.length > 0) {
|
|
1464
|
+
return showRes.details.family.toLowerCase();
|
|
1465
|
+
}
|
|
1466
|
+
const families = showRes.details?.families;
|
|
1467
|
+
if (families && families.length > 0) {
|
|
1468
|
+
const first = families[0];
|
|
1469
|
+
if (typeof first === "string" && first.length > 0) return first.toLowerCase();
|
|
1470
|
+
}
|
|
1471
|
+
return void 0;
|
|
1472
|
+
}
|
|
1473
|
+
var PARALLEL_TOOL_CALL_LOCAL_FAMILIES = /* @__PURE__ */ new Set([
|
|
1474
|
+
"qwen2",
|
|
1475
|
+
"qwen2.5",
|
|
1476
|
+
"qwen3",
|
|
1477
|
+
"llama3.1",
|
|
1478
|
+
"llama3.2",
|
|
1479
|
+
"llama3.3",
|
|
1480
|
+
"llama4",
|
|
1481
|
+
"mistral",
|
|
1482
|
+
"mixtral",
|
|
1483
|
+
"hermes",
|
|
1484
|
+
"command-r",
|
|
1485
|
+
"command-r-plus"
|
|
1486
|
+
]);
|
|
1487
|
+
var PARALLEL_TOOL_CALL_NAME_HINTS = /* @__PURE__ */ new Set([
|
|
1488
|
+
"tool-use",
|
|
1489
|
+
"tooluse",
|
|
1490
|
+
"function-call",
|
|
1491
|
+
"functioncall",
|
|
1492
|
+
"instruct",
|
|
1493
|
+
// Most "instruct" variants of tool-capable families ship with parallel tool calls
|
|
1494
|
+
"hermes",
|
|
1495
|
+
"command-r"
|
|
1496
|
+
]);
|
|
1497
|
+
function inferLocalParallelToolCalls(model, family) {
|
|
1498
|
+
const modelLower = model.toLowerCase();
|
|
1499
|
+
if (family !== void 0 && PARALLEL_TOOL_CALL_LOCAL_FAMILIES.has(family)) {
|
|
1500
|
+
return "yes";
|
|
1501
|
+
}
|
|
1502
|
+
for (const hint of PARALLEL_TOOL_CALL_NAME_HINTS) {
|
|
1503
|
+
if (modelLower.includes(hint)) {
|
|
1504
|
+
return "yes";
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return "unknown";
|
|
1508
|
+
}
|
|
1509
|
+
function inferCloudParallelToolCalls(supportsTools) {
|
|
1510
|
+
return supportsTools ? "yes" : "unknown";
|
|
1511
|
+
}
|
|
1277
1512
|
async function detectModelCapabilities(http, model, signal) {
|
|
1278
1513
|
const showRes = await http.request({
|
|
1279
1514
|
path: "/api/show",
|
|
@@ -1283,10 +1518,16 @@ async function detectModelCapabilities(http, model, signal) {
|
|
|
1283
1518
|
const reported = showRes.capabilities ?? [];
|
|
1284
1519
|
const reportedSet = new Set(reported.map((c) => c.toLowerCase()));
|
|
1285
1520
|
const contextLength = extractContextLength(showRes.model_info);
|
|
1521
|
+
const supportsTools = reportedSet.has("tools");
|
|
1522
|
+
const runtimeMode = inferRuntimeMode(http.baseUrl);
|
|
1523
|
+
const parallelToolCalls = runtimeMode === "cloud" ? inferCloudParallelToolCalls(supportsTools) : runtimeMode === "local" ? inferLocalParallelToolCalls(model, extractModelFamily(showRes)) : (
|
|
1524
|
+
// unknown runtime mode — can't even tell if we're cloud or local
|
|
1525
|
+
"unknown"
|
|
1526
|
+
);
|
|
1286
1527
|
return {
|
|
1287
1528
|
model,
|
|
1288
1529
|
reported,
|
|
1289
|
-
supportsTools
|
|
1530
|
+
supportsTools,
|
|
1290
1531
|
supportsVision: reportedSet.has("vision"),
|
|
1291
1532
|
supportsEmbedding: reportedSet.has("embedding"),
|
|
1292
1533
|
supportsCompletion: reportedSet.has("completion") || !reportedSet.has("embedding"),
|
|
@@ -1294,7 +1535,8 @@ async function detectModelCapabilities(http, model, signal) {
|
|
|
1294
1535
|
...showRes.thinking !== void 0 ? { thinking: showRes.thinking } : {},
|
|
1295
1536
|
...contextLength !== void 0 ? { contextLength } : {},
|
|
1296
1537
|
supportsStreaming: true,
|
|
1297
|
-
supportsStructuredOutputRequest:
|
|
1538
|
+
supportsStructuredOutputRequest: runtimeMode !== "cloud",
|
|
1539
|
+
parallelToolCalls
|
|
1298
1540
|
};
|
|
1299
1541
|
}
|
|
1300
1542
|
async function listAvailableModels(http) {
|
|
@@ -1439,7 +1681,131 @@ function ensureToolCallIds(toolCalls) {
|
|
|
1439
1681
|
return toolCalls.map((tc) => tc.id !== void 0 ? tc : { ...tc, id: generateToolCallId() });
|
|
1440
1682
|
}
|
|
1441
1683
|
|
|
1684
|
+
// src/tools/tool-call-accumulator.ts
|
|
1685
|
+
function isSameToolCall(a, b, aIndex, bIndex) {
|
|
1686
|
+
if (a.id !== void 0 && b.id !== void 0) {
|
|
1687
|
+
return a.id === b.id;
|
|
1688
|
+
}
|
|
1689
|
+
if (aIndex !== void 0 && bIndex !== void 0) {
|
|
1690
|
+
return aIndex === bIndex;
|
|
1691
|
+
}
|
|
1692
|
+
return false;
|
|
1693
|
+
}
|
|
1694
|
+
function mergeToolCall(earlier, later) {
|
|
1695
|
+
const earlierFn = earlier.function;
|
|
1696
|
+
const laterFn = later.function;
|
|
1697
|
+
const earlierArgs = earlierFn.arguments;
|
|
1698
|
+
const laterArgs = laterFn.arguments;
|
|
1699
|
+
const mergedFn = {
|
|
1700
|
+
name: laterFn.name || earlierFn.name,
|
|
1701
|
+
arguments: earlierArgs !== void 0 && laterArgs !== void 0 ? { ...earlierArgs, ...laterArgs } : laterArgs ?? earlierArgs ?? {}
|
|
1702
|
+
};
|
|
1703
|
+
return {
|
|
1704
|
+
// id: prefer the later one if present, else the earlier one
|
|
1705
|
+
...later.id !== void 0 ? { id: later.id } : earlier.id !== void 0 ? { id: earlier.id } : {},
|
|
1706
|
+
function: mergedFn
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
function mergeToolCallArgumentsString(earlier, later) {
|
|
1710
|
+
const earlierArgs = earlier.function.arguments;
|
|
1711
|
+
const laterArgs = later.function.arguments;
|
|
1712
|
+
const concatenated = (typeof earlierArgs === "string" ? earlierArgs : "") + (typeof laterArgs === "string" ? laterArgs : "");
|
|
1713
|
+
return {
|
|
1714
|
+
...later.id !== void 0 ? { id: later.id } : earlier.id !== void 0 ? { id: earlier.id } : {},
|
|
1715
|
+
function: {
|
|
1716
|
+
name: later.function.name || earlier.function.name,
|
|
1717
|
+
arguments: concatenated
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
var ToolCallAccumulator = class {
|
|
1722
|
+
entries = [];
|
|
1723
|
+
/**
|
|
1724
|
+
* Process a chunk's `tool_calls` array. Entries that match an
|
|
1725
|
+
* existing accumulated entry (by id or array position) are merged;
|
|
1726
|
+
* new entries are appended.
|
|
1727
|
+
*
|
|
1728
|
+
* Returns the current accumulated state (a shallow copy — entries
|
|
1729
|
+
* themselves are NOT deep-cloned, so callers that mutate
|
|
1730
|
+
* `function.arguments` on a returned entry will mutate the
|
|
1731
|
+
* accumulator's internal state. Use {@link snapshot} for an
|
|
1732
|
+
* explicitly read-only view, or deep-clone the result if you need
|
|
1733
|
+
* full isolation).
|
|
1734
|
+
*/
|
|
1735
|
+
process(toolCalls) {
|
|
1736
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
1737
|
+
return [...this.entries];
|
|
1738
|
+
}
|
|
1739
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
1740
|
+
const incoming = toolCalls[i];
|
|
1741
|
+
if (!incoming) continue;
|
|
1742
|
+
let matchedIndex = -1;
|
|
1743
|
+
for (let j = 0; j < this.entries.length; j++) {
|
|
1744
|
+
const existing = this.entries[j];
|
|
1745
|
+
if (existing && isSameToolCall(existing, incoming, j, i)) {
|
|
1746
|
+
matchedIndex = j;
|
|
1747
|
+
break;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (matchedIndex >= 0) {
|
|
1751
|
+
this.entries[matchedIndex] = mergeToolCall(this.entries[matchedIndex], incoming);
|
|
1752
|
+
} else {
|
|
1753
|
+
this.entries.push(incoming);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return [...this.entries];
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Returns the final accumulated tool calls. Identical to calling
|
|
1760
|
+
* `process(undefined)` — included for ergonomics so callers can
|
|
1761
|
+
* express "I'm done, give me the result" without a no-op call.
|
|
1762
|
+
*
|
|
1763
|
+
* The returned array is a shallow copy; entries themselves are
|
|
1764
|
+
* the same object references the accumulator holds internally.
|
|
1765
|
+
*/
|
|
1766
|
+
finalize() {
|
|
1767
|
+
return [...this.entries];
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Returns the current accumulated state without modifying the
|
|
1771
|
+
* accumulator. Useful for mid-stream inspection.
|
|
1772
|
+
*
|
|
1773
|
+
* The returned array is a shallow copy; mutating it (e.g.
|
|
1774
|
+
* `result.push(...)`) does not affect the accumulator, but
|
|
1775
|
+
* mutating an entry's `function.arguments` object DOES affect
|
|
1776
|
+
* the accumulator's internal state because the entry reference
|
|
1777
|
+
* is shared. Deep-clone the result if you need full isolation.
|
|
1778
|
+
*/
|
|
1779
|
+
snapshot() {
|
|
1780
|
+
return [...this.entries];
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Returns the number of accumulated tool calls so far.
|
|
1784
|
+
*/
|
|
1785
|
+
get length() {
|
|
1786
|
+
return this.entries.length;
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
function mergeToolCallArrays(earlier, later) {
|
|
1790
|
+
const acc = new ToolCallAccumulator();
|
|
1791
|
+
acc.process(earlier);
|
|
1792
|
+
acc.process(later);
|
|
1793
|
+
return acc.finalize();
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1442
1796
|
// src/streaming/stream.ts
|
|
1797
|
+
function extractPartialContent(accumulated) {
|
|
1798
|
+
if (typeof accumulated !== "object" || accumulated === null) return "";
|
|
1799
|
+
const record = accumulated;
|
|
1800
|
+
const message = record.message;
|
|
1801
|
+
if (typeof message === "object" && message !== null) {
|
|
1802
|
+
const content = message.content;
|
|
1803
|
+
if (typeof content === "string") return content;
|
|
1804
|
+
}
|
|
1805
|
+
const response = record.response;
|
|
1806
|
+
if (typeof response === "string") return response;
|
|
1807
|
+
return "";
|
|
1808
|
+
}
|
|
1443
1809
|
var OllamaStream = class {
|
|
1444
1810
|
constructor(source, mapChunk, aggregate, initial, signal) {
|
|
1445
1811
|
this.source = source;
|
|
@@ -1460,10 +1826,6 @@ var OllamaStream = class {
|
|
|
1460
1826
|
}
|
|
1461
1827
|
}
|
|
1462
1828
|
}
|
|
1463
|
-
source;
|
|
1464
|
-
mapChunk;
|
|
1465
|
-
aggregate;
|
|
1466
|
-
initial;
|
|
1467
1829
|
mode = "unconsumed";
|
|
1468
1830
|
listeners = /* @__PURE__ */ new Map();
|
|
1469
1831
|
finalResultPromise;
|
|
@@ -1522,7 +1884,10 @@ var OllamaStream = class {
|
|
|
1522
1884
|
} catch (error) {
|
|
1523
1885
|
this.removeAbortListener?.();
|
|
1524
1886
|
this.removeAbortListener = void 0;
|
|
1525
|
-
const mapped =
|
|
1887
|
+
const mapped = error instanceof OllamaStreamError && error.partialContent === "" ? new OllamaStreamError(error.message, {
|
|
1888
|
+
partialContent: extractPartialContent(accumulated),
|
|
1889
|
+
cause: error
|
|
1890
|
+
}) : mapError(error);
|
|
1526
1891
|
this.emit({ type: "error", data: { error: mapped } });
|
|
1527
1892
|
this.rejectFinal(mapped);
|
|
1528
1893
|
}
|
|
@@ -1549,7 +1914,10 @@ var OllamaStream = class {
|
|
|
1549
1914
|
}
|
|
1550
1915
|
}
|
|
1551
1916
|
} catch (error) {
|
|
1552
|
-
const mapped =
|
|
1917
|
+
const mapped = error instanceof OllamaStreamError && error.partialContent === "" ? new OllamaStreamError(error.message, {
|
|
1918
|
+
partialContent: extractPartialContent(accumulated),
|
|
1919
|
+
cause: error
|
|
1920
|
+
}) : mapError(error);
|
|
1553
1921
|
this.rejectFinal(mapped);
|
|
1554
1922
|
yield { type: "error", data: { error: mapped } };
|
|
1555
1923
|
} finally {
|
|
@@ -1571,7 +1939,17 @@ function aggregateChat(accumulated, chunk) {
|
|
|
1571
1939
|
role: message.role || accumulated.message.role,
|
|
1572
1940
|
content: accumulated.message.content + (message.content ?? ""),
|
|
1573
1941
|
thinking: message.thinking !== void 0 ? (accumulated.message.thinking ?? "") + message.thinking : accumulated.message.thinking,
|
|
1574
|
-
|
|
1942
|
+
// Defensive tool-call merging: instead of blindly appending
|
|
1943
|
+
// every chunk's tool_calls array (which would produce
|
|
1944
|
+
// duplicates if Ollama ever starts streaming them
|
|
1945
|
+
// incrementally), merge entries that refer to the same logical
|
|
1946
|
+
// call (matched by id or array position). For the documented
|
|
1947
|
+
// native behavior (tool_calls arrive complete in one chunk)
|
|
1948
|
+
// this is a no-op — the accumulator appends exactly once.
|
|
1949
|
+
//
|
|
1950
|
+
// See src/tools/tool-call-accumulator.ts for the full merge
|
|
1951
|
+
// semantics and the rationale.
|
|
1952
|
+
tool_calls: message.tool_calls?.length || accumulated.message.tool_calls?.length ? mergeToolCallArrays(accumulated.message.tool_calls, message.tool_calls) : void 0
|
|
1575
1953
|
},
|
|
1576
1954
|
model: chunk.model,
|
|
1577
1955
|
done: chunk.done,
|
|
@@ -1780,12 +2158,24 @@ function createTimeoutSignal(timeoutMs, userSignal) {
|
|
|
1780
2158
|
return { signal: controller.signal, cancel };
|
|
1781
2159
|
}
|
|
1782
2160
|
|
|
2161
|
+
// src/keep-alive.ts
|
|
2162
|
+
var KEEP_ALIVE_UNLOAD = 0;
|
|
2163
|
+
var KEEP_ALIVE_INDEFINITE = -1;
|
|
2164
|
+
function normalizeKeepAlive(keepAlive) {
|
|
2165
|
+
if (keepAlive === void 0) return void 0;
|
|
2166
|
+
if (keepAlive === "unload") return KEEP_ALIVE_UNLOAD;
|
|
2167
|
+
if (keepAlive === "indefinite") return KEEP_ALIVE_INDEFINITE;
|
|
2168
|
+
return keepAlive;
|
|
2169
|
+
}
|
|
2170
|
+
function isKeepAliveSugar(value) {
|
|
2171
|
+
return value === "unload" || value === "indefinite";
|
|
2172
|
+
}
|
|
2173
|
+
|
|
1783
2174
|
// src/models-client.ts
|
|
1784
2175
|
var ModelsClient = class {
|
|
1785
2176
|
constructor(runner) {
|
|
1786
2177
|
this.runner = runner;
|
|
1787
2178
|
}
|
|
1788
|
-
runner;
|
|
1789
2179
|
list() {
|
|
1790
2180
|
return this.runner((http) => listAvailableModels(http), { singleEndpoint: true });
|
|
1791
2181
|
}
|
|
@@ -1892,6 +2282,76 @@ var ModelsClient = class {
|
|
|
1892
2282
|
{ singleEndpoint: true }
|
|
1893
2283
|
);
|
|
1894
2284
|
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Immediately unloads `model` from VRAM/RAM by issuing an empty
|
|
2287
|
+
* `/api/generate` request with `keep_alive: 0`. The server
|
|
2288
|
+
* finalizes any in-flight generation, releases the model's
|
|
2289
|
+
* weights from GPU memory, and frees the slot for subsequent
|
|
2290
|
+
* pipelines (e.g. swap a vision encoder out before loading an
|
|
2291
|
+
* LLM).
|
|
2292
|
+
*
|
|
2293
|
+
* This is the ergonomic equivalent of:
|
|
2294
|
+
*
|
|
2295
|
+
* ```ts
|
|
2296
|
+
* client.generate({ model, prompt: '', keep_alive: 0 });
|
|
2297
|
+
* ```
|
|
2298
|
+
*
|
|
2299
|
+
* Use {@link pin} for the opposite lifecycle — pinning a model
|
|
2300
|
+
* indefinitely in VRAM for hot-loop inference.
|
|
2301
|
+
*
|
|
2302
|
+
* Resolves once the server acknowledges the unload. The promise
|
|
2303
|
+
* rejects on transport errors, model-not-found (404), or auth
|
|
2304
|
+
* failures — callers handling transient issues can retry via the
|
|
2305
|
+
* standard {@link withRetry} policy.
|
|
2306
|
+
*
|
|
2307
|
+
* See: https://github.com/ollama/ollama/blob/main/docs/faq.md
|
|
2308
|
+
* #how-do-i-keep-a-model-loaded-in-memory-or-make-it-unload-immediately
|
|
2309
|
+
*/
|
|
2310
|
+
async unload(model) {
|
|
2311
|
+
await this.runner(
|
|
2312
|
+
(http, signal) => http.request({
|
|
2313
|
+
path: "/api/generate",
|
|
2314
|
+
body: { model, prompt: "", keep_alive: KEEP_ALIVE_UNLOAD, stream: false },
|
|
2315
|
+
signal
|
|
2316
|
+
}),
|
|
2317
|
+
{ model, singleEndpoint: true }
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Pre-loads and pins `model` into VRAM indefinitely by issuing an
|
|
2322
|
+
* empty `/api/generate` request with `keep_alive: -1`. The server
|
|
2323
|
+
* loads the model into GPU memory (paying the cold-load latency
|
|
2324
|
+
* once) and keeps it resident until either:
|
|
2325
|
+
*
|
|
2326
|
+
* - an explicit {@link unload} call is made,
|
|
2327
|
+
* - the server process is restarted, or
|
|
2328
|
+
* - the host runs out of VRAM and the server's own eviction
|
|
2329
|
+
* policy kicks in.
|
|
2330
|
+
*
|
|
2331
|
+
* This is the ergonomic equivalent of:
|
|
2332
|
+
*
|
|
2333
|
+
* ```ts
|
|
2334
|
+
* client.generate({ model, prompt: '', keep_alive: -1 });
|
|
2335
|
+
* ```
|
|
2336
|
+
*
|
|
2337
|
+
* Use {@link unload} to release the pinned slot when the hot loop
|
|
2338
|
+
* is done — leaving models pinned indefinitely exhausts VRAM and
|
|
2339
|
+
* starves subsequent pipelines.
|
|
2340
|
+
*
|
|
2341
|
+
* Resolves once the server acknowledges the pin (i.e. the model
|
|
2342
|
+
* is fully loaded). The promise rejects on transport errors,
|
|
2343
|
+
* model-not-found (404), or VRAM-exhaustion failures.
|
|
2344
|
+
*/
|
|
2345
|
+
async pin(model) {
|
|
2346
|
+
await this.runner(
|
|
2347
|
+
(http, signal) => http.request({
|
|
2348
|
+
path: "/api/generate",
|
|
2349
|
+
body: { model, prompt: "", keep_alive: KEEP_ALIVE_INDEFINITE, stream: false },
|
|
2350
|
+
signal
|
|
2351
|
+
}),
|
|
2352
|
+
{ model, singleEndpoint: true }
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
1895
2355
|
async createBlob(digest, data) {
|
|
1896
2356
|
await this.runner(
|
|
1897
2357
|
(http, signal) => http.request({
|
|
@@ -1919,6 +2379,100 @@ var ModelsClient = class {
|
|
|
1919
2379
|
throw err;
|
|
1920
2380
|
}
|
|
1921
2381
|
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Computes a blob digest the way Ollama's content-addressed store expects:
|
|
2384
|
+
* SHA-256 over the raw bytes, formatted as `sha256:<64 lowercase hex>`.
|
|
2385
|
+
*
|
|
2386
|
+
* Uses the Web Crypto API (`crypto.subtle.digest`), which exists in every
|
|
2387
|
+
* supported runtime — Node.js ≥ 19 exposes it as a global (this SDK
|
|
2388
|
+
* requires Node ≥ 20), and browsers/edge runtimes have always had it.
|
|
2389
|
+
*/
|
|
2390
|
+
async computeBlobDigest(data) {
|
|
2391
|
+
const subtle = globalThis.crypto?.subtle;
|
|
2392
|
+
if (subtle === void 0) {
|
|
2393
|
+
throw new OllamaClientError(
|
|
2394
|
+
"computeBlobDigest requires the Web Crypto API (crypto.subtle), unavailable in this runtime.",
|
|
2395
|
+
{ code: "crypto_unavailable" }
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
const digestBytes = await subtle.digest("SHA-256", data);
|
|
2399
|
+
const hex = Array.from(
|
|
2400
|
+
new Uint8Array(digestBytes),
|
|
2401
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
2402
|
+
).join("");
|
|
2403
|
+
return `sha256:${hex}`;
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Computes the SHA-256 digest of `data` and pushes it as a blob in one
|
|
2407
|
+
* step — the content-addressed upload Ollama's own tooling performs
|
|
2408
|
+
* (`curl -T file http://…/api/blobs/sha256:…`). Returns the digest, which
|
|
2409
|
+
* `/api/create`'s `files` field references.
|
|
2410
|
+
*
|
|
2411
|
+
* Pass `digest` to skip recomputation when the caller already knows it —
|
|
2412
|
+
* the server verifies the content against the digest and rejects mismatches.
|
|
2413
|
+
*/
|
|
2414
|
+
async createBlobFromData(data, opts) {
|
|
2415
|
+
const digest = opts?.digest ?? await this.computeBlobDigest(data);
|
|
2416
|
+
const alreadyExisted = await this.checkBlob(digest);
|
|
2417
|
+
if (!alreadyExisted) {
|
|
2418
|
+
await this.createBlob(digest, data);
|
|
2419
|
+
}
|
|
2420
|
+
return { digest, alreadyExisted };
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Reads a local file and uploads it as a blob (Node.js only — dynamic
|
|
2424
|
+
* `node:fs` import, so browser bundles never pull it in). The file name
|
|
2425
|
+
* (basename) is what `/api/create`'s `files` map expects as the key, so
|
|
2426
|
+
* this returns it alongside the digest.
|
|
2427
|
+
*/
|
|
2428
|
+
async createBlobFromFile(path, opts) {
|
|
2429
|
+
const nodeFsModule = "node:fs";
|
|
2430
|
+
const { promises: fs } = await import(nodeFsModule);
|
|
2431
|
+
const data = await fs.readFile(path);
|
|
2432
|
+
const result = await this.createBlobFromData(new Uint8Array(data), {
|
|
2433
|
+
...opts
|
|
2434
|
+
});
|
|
2435
|
+
const fileName = path.split(/[\\/]/).pop() ?? path;
|
|
2436
|
+
return { ...result, fileName };
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* One-shot custom-model publishing from raw GGUF weights, following the
|
|
2440
|
+
* protocol Ollama's API reference documents: push a blob for the GGUF
|
|
2441
|
+
* bytes, then `POST /api/create` with `files: { <fileName>: <digest> }`.
|
|
2442
|
+
* Split GGUFs are supported via `files` — pass every shard's path.
|
|
2443
|
+
*
|
|
2444
|
+
* ```ts
|
|
2445
|
+
* await client.models.createModelFromGguf('my-model', '/path/to/model.gguf', {
|
|
2446
|
+
* template: '{{ .Prompt }}',
|
|
2447
|
+
* });
|
|
2448
|
+
* ```
|
|
2449
|
+
*
|
|
2450
|
+
* @param model Name for the new model (e.g. `'my-model:latest'`).
|
|
2451
|
+
* @param gguf Path(s) to GGUF file(s) on the local filesystem.
|
|
2452
|
+
* @param opts Everything else `/api/create` accepts (`template`, `system`,
|
|
2453
|
+
* `parameters`, `license`, `quantize`, …) except `files` and `from`.
|
|
2454
|
+
*/
|
|
2455
|
+
async createModelFromGguf(model, gguf, opts) {
|
|
2456
|
+
const paths = typeof gguf === "string" ? [gguf] : gguf;
|
|
2457
|
+
if (paths.length === 0) {
|
|
2458
|
+
throw new OllamaClientError("createModelFromGguf: at least one GGUF path is required.", {
|
|
2459
|
+
code: "invalid_request"
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
const files = {};
|
|
2463
|
+
for (const path of paths) {
|
|
2464
|
+
const uploaded = await this.createBlobFromFile(path, { signal: opts?.signal });
|
|
2465
|
+
files[uploaded.fileName] = uploaded.digest;
|
|
2466
|
+
}
|
|
2467
|
+
const {
|
|
2468
|
+
model: _model,
|
|
2469
|
+
files: _files,
|
|
2470
|
+
from: _from,
|
|
2471
|
+
stream: _stream,
|
|
2472
|
+
...rest
|
|
2473
|
+
} = opts ?? {};
|
|
2474
|
+
return this.create({ ...rest, model, files, stream: false });
|
|
2475
|
+
}
|
|
1922
2476
|
};
|
|
1923
2477
|
|
|
1924
2478
|
// src/integrations/openai.ts
|
|
@@ -1945,7 +2499,6 @@ var OpenAIChatCompletionStream = class {
|
|
|
1945
2499
|
}
|
|
1946
2500
|
}
|
|
1947
2501
|
}
|
|
1948
|
-
source;
|
|
1949
2502
|
finalResultPromise;
|
|
1950
2503
|
resolveFinal;
|
|
1951
2504
|
rejectFinal;
|
|
@@ -2089,7 +2642,6 @@ var OpenAICompletionStream = class {
|
|
|
2089
2642
|
}
|
|
2090
2643
|
}
|
|
2091
2644
|
}
|
|
2092
|
-
source;
|
|
2093
2645
|
finalResultPromise;
|
|
2094
2646
|
resolveFinal;
|
|
2095
2647
|
rejectFinal;
|
|
@@ -2170,7 +2722,6 @@ var OpenAIResponsesStream = class {
|
|
|
2170
2722
|
}
|
|
2171
2723
|
}
|
|
2172
2724
|
}
|
|
2173
|
-
source;
|
|
2174
2725
|
finalResultPromise;
|
|
2175
2726
|
resolveFinal;
|
|
2176
2727
|
rejectFinal;
|
|
@@ -2395,8 +2946,6 @@ var OpenAICompatClient = class {
|
|
|
2395
2946
|
this.http = http;
|
|
2396
2947
|
this.runner = runner;
|
|
2397
2948
|
}
|
|
2398
|
-
http;
|
|
2399
|
-
runner;
|
|
2400
2949
|
request(operation, model, signal, holdUntil) {
|
|
2401
2950
|
if (this.runner) {
|
|
2402
2951
|
return this.runner(
|
|
@@ -2616,7 +3165,6 @@ var AnthropicMessagesStream = class {
|
|
|
2616
3165
|
}
|
|
2617
3166
|
}
|
|
2618
3167
|
}
|
|
2619
|
-
source;
|
|
2620
3168
|
finalResultPromise;
|
|
2621
3169
|
resolveFinal;
|
|
2622
3170
|
rejectFinal;
|
|
@@ -2731,8 +3279,6 @@ var AnthropicCompatClient = class {
|
|
|
2731
3279
|
this.http = http;
|
|
2732
3280
|
this.runner = runner;
|
|
2733
3281
|
}
|
|
2734
|
-
http;
|
|
2735
|
-
runner;
|
|
2736
3282
|
request(operation, model, signal, holdUntil) {
|
|
2737
3283
|
if (this.runner) {
|
|
2738
3284
|
return this.runner(
|
|
@@ -2778,6 +3324,32 @@ var AnthropicCompatClient = class {
|
|
|
2778
3324
|
return this.createMessage(request, signal);
|
|
2779
3325
|
}
|
|
2780
3326
|
};
|
|
3327
|
+
var IncludedBalanceSchema = z.object({
|
|
3328
|
+
balance_usd: z.number(),
|
|
3329
|
+
allowance_usd: z.number(),
|
|
3330
|
+
period: z.object({
|
|
3331
|
+
from: z.string(),
|
|
3332
|
+
until: z.string()
|
|
3333
|
+
})
|
|
3334
|
+
});
|
|
3335
|
+
var LegacyBalanceLimitSchema = z.object({
|
|
3336
|
+
remaining_percent: z.number().min(0).max(100),
|
|
3337
|
+
resets_at: z.string()
|
|
3338
|
+
});
|
|
3339
|
+
|
|
3340
|
+
// src/generated/models/LegacyIncludedBalance.schema.ts
|
|
3341
|
+
var LegacyIncludedBalanceSchema = z.object({
|
|
3342
|
+
session: LegacyBalanceLimitSchema,
|
|
3343
|
+
weekly: LegacyBalanceLimitSchema
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3346
|
+
// src/generated/models/BalanceResponse.schema.ts
|
|
3347
|
+
var BalanceResponseSchema = z.object({
|
|
3348
|
+
included: z.union([IncludedBalanceSchema, LegacyIncludedBalanceSchema]),
|
|
3349
|
+
purchased: z.object({
|
|
3350
|
+
balance_usd: z.number()
|
|
3351
|
+
})
|
|
3352
|
+
});
|
|
2781
3353
|
var ToolCallSchema = z.object({
|
|
2782
3354
|
function: z.object({
|
|
2783
3355
|
name: z.string(),
|
|
@@ -2881,6 +3453,7 @@ var CreateRequestSchema = z.object({
|
|
|
2881
3453
|
quantize: z.string().optional(),
|
|
2882
3454
|
draft_quantize: z.string().optional(),
|
|
2883
3455
|
requires: z.string().optional(),
|
|
3456
|
+
capabilities: z.array(z.string()).optional(),
|
|
2884
3457
|
stream: z.boolean().optional()
|
|
2885
3458
|
});
|
|
2886
3459
|
var DeleteRequestSchema = z.object({
|
|
@@ -2943,7 +3516,7 @@ var ModelSummarySchema = z.object({
|
|
|
2943
3516
|
details: z.object({
|
|
2944
3517
|
format: z.string().optional(),
|
|
2945
3518
|
family: z.string().optional(),
|
|
2946
|
-
families: z.array(z.string()).optional(),
|
|
3519
|
+
families: z.union([z.array(z.string()), z.null()]).optional(),
|
|
2947
3520
|
parameter_size: z.string().optional(),
|
|
2948
3521
|
quantization_level: z.string().optional()
|
|
2949
3522
|
}).optional()
|
|
@@ -3078,6 +3651,34 @@ var SystemOneResponseSchema = z.object({
|
|
|
3078
3651
|
output_tokens: z.number().min(0)
|
|
3079
3652
|
})
|
|
3080
3653
|
});
|
|
3654
|
+
var UsageMetricsSchema = z.object({
|
|
3655
|
+
request_count: z.number().min(0),
|
|
3656
|
+
usage_usd: z.number().optional(),
|
|
3657
|
+
input_tokens: z.number().optional(),
|
|
3658
|
+
cached_input_tokens: z.number().optional(),
|
|
3659
|
+
output_tokens: z.number().optional()
|
|
3660
|
+
});
|
|
3661
|
+
|
|
3662
|
+
// src/generated/models/UsageBucket.schema.ts
|
|
3663
|
+
var UsageBucketSchema = z.intersection(
|
|
3664
|
+
UsageMetricsSchema,
|
|
3665
|
+
z.object({
|
|
3666
|
+
from: z.string(),
|
|
3667
|
+
until: z.string(),
|
|
3668
|
+
partial: z.boolean().optional()
|
|
3669
|
+
})
|
|
3670
|
+
);
|
|
3671
|
+
|
|
3672
|
+
// src/generated/models/UsageResponse.schema.ts
|
|
3673
|
+
var UsageResponseSchema = z.object({
|
|
3674
|
+
range: z.enum(["24h", "7d", "30d"]),
|
|
3675
|
+
scope: z.enum(["self", "team"]),
|
|
3676
|
+
granularity: z.enum(["hour", "day"]),
|
|
3677
|
+
from: z.string(),
|
|
3678
|
+
until: z.string(),
|
|
3679
|
+
totals: UsageMetricsSchema,
|
|
3680
|
+
buckets: z.array(UsageBucketSchema)
|
|
3681
|
+
});
|
|
3081
3682
|
var VersionResponseSchema = z.object({
|
|
3082
3683
|
version: z.string().optional()
|
|
3083
3684
|
});
|
|
@@ -3120,6 +3721,7 @@ var requestSchemas = {
|
|
|
3120
3721
|
webSearch: WebSearchRequestSchema
|
|
3121
3722
|
};
|
|
3122
3723
|
var responseSchemas = {
|
|
3724
|
+
balance: BalanceResponseSchema,
|
|
3123
3725
|
chat: ChatResponseSchema,
|
|
3124
3726
|
create: StatusResponseSchema,
|
|
3125
3727
|
embed: EmbedResponseSchema,
|
|
@@ -3130,6 +3732,7 @@ var responseSchemas = {
|
|
|
3130
3732
|
show: ShowResponseSchema,
|
|
3131
3733
|
systemOne: SystemOneResponseSchema,
|
|
3132
3734
|
tags: ListResponseSchema,
|
|
3735
|
+
usage: UsageResponseSchema,
|
|
3133
3736
|
version: VersionResponseSchema,
|
|
3134
3737
|
webFetch: WebFetchResponseSchema,
|
|
3135
3738
|
webSearch: WebSearchResponseSchema
|
|
@@ -3180,7 +3783,6 @@ var OllamaRuntime = class {
|
|
|
3180
3783
|
constructor(options) {
|
|
3181
3784
|
this.options = options;
|
|
3182
3785
|
}
|
|
3183
|
-
options;
|
|
3184
3786
|
/**
|
|
3185
3787
|
* Cached promise of the Ollama server version, populated lazily the first
|
|
3186
3788
|
* time a version-gated operation is invoked. `undefined` until then.
|
|
@@ -3257,11 +3859,19 @@ var OllamaRuntime = class {
|
|
|
3257
3859
|
}
|
|
3258
3860
|
const streaming = shouldStream(req);
|
|
3259
3861
|
const resolvedPath = resolvePathParams(req.operation.path, req.pathParams);
|
|
3862
|
+
const pathWithQuery = applyQueryParams(resolvedPath, req.queryParams);
|
|
3260
3863
|
const model = req.model ?? extractModelFromBody(body);
|
|
3864
|
+
const headers = {};
|
|
3865
|
+
if (req.headerParams) {
|
|
3866
|
+
for (const [key, value] of Object.entries(req.headerParams)) {
|
|
3867
|
+
headers[key] = value;
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3261
3870
|
const httpReq = {
|
|
3262
|
-
path:
|
|
3871
|
+
path: pathWithQuery,
|
|
3263
3872
|
method: req.operation.method,
|
|
3264
3873
|
...body !== void 0 ? { body } : {},
|
|
3874
|
+
...Object.keys(headers).length > 0 ? { headers } : {},
|
|
3265
3875
|
...req.signal !== void 0 ? { signal: req.signal } : {},
|
|
3266
3876
|
...streaming ? { raw: true } : {},
|
|
3267
3877
|
...model !== void 0 ? { model } : {}
|
|
@@ -3402,6 +4012,465 @@ function extractModelFromBody(body) {
|
|
|
3402
4012
|
const model = obj.model;
|
|
3403
4013
|
return typeof model === "string" ? model : void 0;
|
|
3404
4014
|
}
|
|
4015
|
+
function applyQueryParams(path, queryParams) {
|
|
4016
|
+
if (!queryParams || Object.keys(queryParams).length === 0) return path;
|
|
4017
|
+
const pairs = Object.entries(queryParams).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
|
|
4018
|
+
return `${path}?${pairs}`;
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
// src/responses.ts
|
|
4022
|
+
var adapterResponseSequence = 0;
|
|
4023
|
+
function toNativeTools(tools) {
|
|
4024
|
+
if (tools === void 0 || tools.length === 0) return void 0;
|
|
4025
|
+
return tools.map((tool) => ({
|
|
4026
|
+
type: "function",
|
|
4027
|
+
function: {
|
|
4028
|
+
name: tool.name,
|
|
4029
|
+
description: tool.description ?? "",
|
|
4030
|
+
parameters: tool.parameters ?? { type: "object", properties: {} }
|
|
4031
|
+
}
|
|
4032
|
+
}));
|
|
4033
|
+
}
|
|
4034
|
+
function mapNativeUsage(usage) {
|
|
4035
|
+
if (usage === void 0) return void 0;
|
|
4036
|
+
const input = usage.input_tokens ?? 0;
|
|
4037
|
+
const output = usage.output_tokens ?? 0;
|
|
4038
|
+
return { input_tokens: input, output_tokens: output, total_tokens: input + output };
|
|
4039
|
+
}
|
|
4040
|
+
function parseToolCallArguments(raw) {
|
|
4041
|
+
if (raw === void 0 || raw === null || raw === "") return {};
|
|
4042
|
+
try {
|
|
4043
|
+
const parsed = JSON.parse(raw);
|
|
4044
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
4045
|
+
} catch {
|
|
4046
|
+
return { _raw: raw };
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
function mapNativeResponse(native) {
|
|
4050
|
+
const textParts = [];
|
|
4051
|
+
const thinkingParts = [];
|
|
4052
|
+
const toolCalls = [];
|
|
4053
|
+
for (const item of native.output ?? []) {
|
|
4054
|
+
if (item.type === "message") {
|
|
4055
|
+
for (const part of item.content ?? []) {
|
|
4056
|
+
if (part.type === "output_text") textParts.push(part.text ?? "");
|
|
4057
|
+
else if (part.type === "refusal") textParts.push(part.refusal ?? "");
|
|
4058
|
+
}
|
|
4059
|
+
} else if (item.type === "reasoning") {
|
|
4060
|
+
if (item.text !== void 0 && item.text !== "") thinkingParts.push(item.text);
|
|
4061
|
+
} else if (item.type === "function_call") {
|
|
4062
|
+
toolCalls.push({
|
|
4063
|
+
...item.id !== void 0 ? { id: item.id } : {},
|
|
4064
|
+
function: { name: item.name, arguments: parseToolCallArguments(item.arguments) }
|
|
4065
|
+
});
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
return {
|
|
4069
|
+
id: native.id ?? `resp_${Date.now().toString(36)}`,
|
|
4070
|
+
model: native.model,
|
|
4071
|
+
created_at: new Date((native.created_at ?? 0) * 1e3).toISOString(),
|
|
4072
|
+
output_text: textParts.join(""),
|
|
4073
|
+
...thinkingParts.length > 0 ? { thinking: thinkingParts.join("\n") } : {},
|
|
4074
|
+
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {},
|
|
4075
|
+
...mapNativeUsage(native.usage) !== void 0 ? { usage: mapNativeUsage(native.usage) } : {}
|
|
4076
|
+
};
|
|
4077
|
+
}
|
|
4078
|
+
function mapChatUsage(res) {
|
|
4079
|
+
const asChat = res;
|
|
4080
|
+
const asStream = res;
|
|
4081
|
+
const input = asChat.prompt_eval_count ?? asStream.usage?.promptTokens ?? asStream.raw?.prompt_eval_count ?? 0;
|
|
4082
|
+
const output = asChat.eval_count ?? asStream.usage?.completionTokens ?? asStream.raw?.eval_count ?? 0;
|
|
4083
|
+
if (input === 0 && output === 0) return void 0;
|
|
4084
|
+
return { input_tokens: input, output_tokens: output, total_tokens: input + output };
|
|
4085
|
+
}
|
|
4086
|
+
function mapChatResponse(res) {
|
|
4087
|
+
adapterResponseSequence += 1;
|
|
4088
|
+
return {
|
|
4089
|
+
id: `resp_${Date.now().toString(36)}${adapterResponseSequence.toString(36)}`,
|
|
4090
|
+
model: res.model,
|
|
4091
|
+
created_at: res.created_at,
|
|
4092
|
+
output_text: res.message.content,
|
|
4093
|
+
...res.message.thinking !== void 0 ? { thinking: res.message.thinking } : {},
|
|
4094
|
+
...res.message.tool_calls !== void 0 && res.message.tool_calls.length > 0 ? { tool_calls: res.message.tool_calls } : {},
|
|
4095
|
+
...mapChatUsage(res) !== void 0 ? { usage: mapChatUsage(res) } : {},
|
|
4096
|
+
transport: "chat-adapter"
|
|
4097
|
+
};
|
|
4098
|
+
}
|
|
4099
|
+
var ResponsesModule = class {
|
|
4100
|
+
constructor(runner) {
|
|
4101
|
+
this.runner = runner;
|
|
4102
|
+
}
|
|
4103
|
+
/** The request body sent to the native endpoint (also reused for streaming). */
|
|
4104
|
+
nativeBody(req) {
|
|
4105
|
+
return {
|
|
4106
|
+
model: req.model,
|
|
4107
|
+
input: req.input,
|
|
4108
|
+
...req.instructions !== void 0 ? { instructions: req.instructions } : {},
|
|
4109
|
+
...req.tools !== void 0 && req.tools.length > 0 ? { tools: toNativeTools(req.tools) } : {},
|
|
4110
|
+
...req.temperature !== void 0 ? { temperature: req.temperature } : {},
|
|
4111
|
+
...req.top_p !== void 0 ? { top_p: req.top_p } : {},
|
|
4112
|
+
...req.max_output_tokens !== void 0 ? { max_output_tokens: req.max_output_tokens } : {},
|
|
4113
|
+
...req.reasoning_effort !== void 0 ? { reasoning: { effort: req.reasoning_effort } } : {},
|
|
4114
|
+
...req.think !== void 0 ? { think: req.think } : {}
|
|
4115
|
+
};
|
|
4116
|
+
}
|
|
4117
|
+
/**
|
|
4118
|
+
* Runs one Responses request, preferring the native `POST /v1/responses`
|
|
4119
|
+
* endpoint and transparently re-issuing via `/api/chat` when the server
|
|
4120
|
+
* answers `404` (pre-v0.13.3 Ollama without the compatibility endpoint).
|
|
4121
|
+
*/
|
|
4122
|
+
async create(req) {
|
|
4123
|
+
const body = this.nativeBody(req);
|
|
4124
|
+
try {
|
|
4125
|
+
const native = await this.runner(
|
|
4126
|
+
(http, signal) => http.request({
|
|
4127
|
+
path: "/v1/responses",
|
|
4128
|
+
body: { ...body, stream: false },
|
|
4129
|
+
signal
|
|
4130
|
+
}),
|
|
4131
|
+
{ model: req.model, signal: req.signal, timeoutMs: req.timeoutMs }
|
|
4132
|
+
);
|
|
4133
|
+
return { ...mapNativeResponse(native), transport: "native" };
|
|
4134
|
+
} catch (err) {
|
|
4135
|
+
if (!(err instanceof OllamaNotFoundError)) throw err;
|
|
4136
|
+
}
|
|
4137
|
+
return this.createViaChatAdapter(req);
|
|
4138
|
+
}
|
|
4139
|
+
/** Convenience: `create()` returning only the `output_text` string. */
|
|
4140
|
+
async createText(req) {
|
|
4141
|
+
const res = await this.create(req);
|
|
4142
|
+
return res.output_text;
|
|
4143
|
+
}
|
|
4144
|
+
async createViaChatAdapter(req) {
|
|
4145
|
+
const messages = [
|
|
4146
|
+
...req.instructions !== void 0 ? [{ role: "system", content: req.instructions }] : [],
|
|
4147
|
+
{ role: "user", content: req.input }
|
|
4148
|
+
];
|
|
4149
|
+
const chatRes = await this.runner(
|
|
4150
|
+
(http, signal) => http.request({
|
|
4151
|
+
path: "/api/chat",
|
|
4152
|
+
body: {
|
|
4153
|
+
model: req.model,
|
|
4154
|
+
messages,
|
|
4155
|
+
...req.tools !== void 0 && req.tools.length > 0 ? { tools: toNativeTools(req.tools) } : {},
|
|
4156
|
+
options: {
|
|
4157
|
+
...req.temperature !== void 0 ? { temperature: req.temperature } : {},
|
|
4158
|
+
...req.top_p !== void 0 ? { top_p: req.top_p } : {},
|
|
4159
|
+
...req.max_output_tokens !== void 0 ? { num_predict: req.max_output_tokens } : {}
|
|
4160
|
+
},
|
|
4161
|
+
...req.think !== void 0 ? { think: req.think } : {},
|
|
4162
|
+
stream: false
|
|
4163
|
+
},
|
|
4164
|
+
signal
|
|
4165
|
+
}),
|
|
4166
|
+
{ model: req.model, signal: req.signal, timeoutMs: req.timeoutMs }
|
|
4167
|
+
);
|
|
4168
|
+
return mapChatResponse(chatRes);
|
|
4169
|
+
}
|
|
4170
|
+
/**
|
|
4171
|
+
* Streams a Responses request as simplified
|
|
4172
|
+
* `{type: 'text_delta' | 'thinking_delta' | 'done'}` events. Dual-mode like
|
|
4173
|
+
* {@link create}: native SSE from `/v1/responses` when available, otherwise
|
|
4174
|
+
* the normalized `/api/chat` token stream. Tool calls are not delta'd —
|
|
4175
|
+
* collect them from the final `done` event's `response.tool_calls`.
|
|
4176
|
+
*
|
|
4177
|
+
* This is a lazy async generator: nothing is sent until the first
|
|
4178
|
+
* iteration, matching `for await (... of client.responses.stream(req))`
|
|
4179
|
+
* intuition. The generator is also cancellable through `req.signal`.
|
|
4180
|
+
*/
|
|
4181
|
+
async *stream(req) {
|
|
4182
|
+
const body = this.nativeBody(req);
|
|
4183
|
+
let nativeStream;
|
|
4184
|
+
try {
|
|
4185
|
+
nativeStream = await this.runner(
|
|
4186
|
+
async (http, signal) => {
|
|
4187
|
+
const source = await http.requestSseStream({
|
|
4188
|
+
path: "/v1/responses",
|
|
4189
|
+
body: { ...body, stream: true },
|
|
4190
|
+
signal
|
|
4191
|
+
});
|
|
4192
|
+
return new OpenAIResponsesStream(source, signal);
|
|
4193
|
+
},
|
|
4194
|
+
{
|
|
4195
|
+
model: req.model,
|
|
4196
|
+
signal: req.signal,
|
|
4197
|
+
timeoutMs: req.timeoutMs,
|
|
4198
|
+
holdUntil: (s) => s.finalResult
|
|
4199
|
+
}
|
|
4200
|
+
);
|
|
4201
|
+
} catch (err) {
|
|
4202
|
+
if (!(err instanceof OllamaNotFoundError)) throw err;
|
|
4203
|
+
}
|
|
4204
|
+
if (nativeStream !== void 0) {
|
|
4205
|
+
for await (const event of nativeStream) {
|
|
4206
|
+
const type = event.type;
|
|
4207
|
+
if (type === "response.output_text.delta") {
|
|
4208
|
+
yield { type: "text_delta", delta: event.delta };
|
|
4209
|
+
} else if (type === "response.reasoning_text.delta") {
|
|
4210
|
+
yield { type: "thinking_delta", delta: event.delta };
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
yield {
|
|
4214
|
+
type: "done",
|
|
4215
|
+
response: { ...mapNativeResponse(await nativeStream.finalResult), transport: "native" }
|
|
4216
|
+
};
|
|
4217
|
+
return;
|
|
4218
|
+
}
|
|
4219
|
+
const chatStream = await this.runner(
|
|
4220
|
+
async (http, signal) => {
|
|
4221
|
+
const source = await http.requestStream({
|
|
4222
|
+
path: "/api/chat",
|
|
4223
|
+
body: {
|
|
4224
|
+
model: req.model,
|
|
4225
|
+
messages: [
|
|
4226
|
+
...req.instructions !== void 0 ? [{ role: "system", content: req.instructions }] : [],
|
|
4227
|
+
{ role: "user", content: req.input }
|
|
4228
|
+
],
|
|
4229
|
+
...req.tools !== void 0 && req.tools.length > 0 ? { tools: toNativeTools(req.tools) } : {},
|
|
4230
|
+
options: {
|
|
4231
|
+
...req.temperature !== void 0 ? { temperature: req.temperature } : {},
|
|
4232
|
+
...req.top_p !== void 0 ? { top_p: req.top_p } : {},
|
|
4233
|
+
...req.max_output_tokens !== void 0 ? { num_predict: req.max_output_tokens } : {}
|
|
4234
|
+
},
|
|
4235
|
+
...req.think !== void 0 ? { think: req.think } : {},
|
|
4236
|
+
stream: true
|
|
4237
|
+
},
|
|
4238
|
+
signal
|
|
4239
|
+
});
|
|
4240
|
+
return normalizeChatStream(source, signal);
|
|
4241
|
+
},
|
|
4242
|
+
{
|
|
4243
|
+
model: req.model,
|
|
4244
|
+
signal: req.signal,
|
|
4245
|
+
timeoutMs: req.timeoutMs,
|
|
4246
|
+
holdUntil: (s) => s.finalResult
|
|
4247
|
+
}
|
|
4248
|
+
);
|
|
4249
|
+
for await (const event of chatStream) {
|
|
4250
|
+
if (event.type === "token") {
|
|
4251
|
+
yield { type: "text_delta", delta: event.data.delta };
|
|
4252
|
+
} else if (event.type === "thinking") {
|
|
4253
|
+
yield { type: "thinking_delta", delta: event.data.delta };
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
const result = await chatStream.finalResult;
|
|
4257
|
+
yield {
|
|
4258
|
+
type: "done",
|
|
4259
|
+
response: {
|
|
4260
|
+
id: `resp_${Date.now().toString(36)}${(++adapterResponseSequence).toString(36)}`,
|
|
4261
|
+
model: result.model,
|
|
4262
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4263
|
+
output_text: result.message.content,
|
|
4264
|
+
...result.message.thinking !== void 0 ? { thinking: result.message.thinking } : {},
|
|
4265
|
+
...result.message.tool_calls !== void 0 && result.message.tool_calls.length > 0 ? { tool_calls: result.message.tool_calls } : {},
|
|
4266
|
+
...mapChatUsage(result) !== void 0 ? { usage: mapChatUsage(result) } : {},
|
|
4267
|
+
transport: "chat-adapter"
|
|
4268
|
+
}
|
|
4269
|
+
};
|
|
4270
|
+
}
|
|
4271
|
+
};
|
|
4272
|
+
|
|
4273
|
+
// src/conversation.ts
|
|
4274
|
+
function extractTurnStats(res) {
|
|
4275
|
+
const cachedTokens = res.prompt_eval_cached_count ?? 0;
|
|
4276
|
+
const evaluatedTokens = res.prompt_eval_count ?? 0;
|
|
4277
|
+
const total = cachedTokens + evaluatedTokens;
|
|
4278
|
+
return {
|
|
4279
|
+
cachedTokens,
|
|
4280
|
+
evaluatedTokens,
|
|
4281
|
+
hitRate: total > 0 ? cachedTokens / total : 0
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
var ConversationSession = class {
|
|
4285
|
+
constructor(client, config) {
|
|
4286
|
+
this.client = client;
|
|
4287
|
+
this.config = config;
|
|
4288
|
+
if (config.systemPrompt !== void 0 && config.systemPrompt !== "") {
|
|
4289
|
+
this.history.push(Object.freeze({ role: "system", content: config.systemPrompt }));
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
/** Ordered, append-only history. Message objects are frozen on push. */
|
|
4293
|
+
history = [];
|
|
4294
|
+
/** Mutable tallies behind the read-only {@link cacheStats} view. */
|
|
4295
|
+
tallies = {
|
|
4296
|
+
turns: 0,
|
|
4297
|
+
cachedTokens: 0,
|
|
4298
|
+
evaluatedTokens: 0
|
|
4299
|
+
};
|
|
4300
|
+
/** The model name this session talks to. */
|
|
4301
|
+
get model() {
|
|
4302
|
+
return this.config.model;
|
|
4303
|
+
}
|
|
4304
|
+
/** Number of completed turns. */
|
|
4305
|
+
get turnCount() {
|
|
4306
|
+
return this.tallies.turns;
|
|
4307
|
+
}
|
|
4308
|
+
/** Cumulative KV-cache statistics across completed turns. */
|
|
4309
|
+
get cacheStats() {
|
|
4310
|
+
const total = this.tallies.cachedTokens + this.tallies.evaluatedTokens;
|
|
4311
|
+
return {
|
|
4312
|
+
turns: this.tallies.turns,
|
|
4313
|
+
cachedTokens: this.tallies.cachedTokens,
|
|
4314
|
+
evaluatedTokens: this.tallies.evaluatedTokens,
|
|
4315
|
+
totalPromptTokens: total,
|
|
4316
|
+
hitRate: total > 0 ? this.tallies.cachedTokens / total : 0
|
|
4317
|
+
};
|
|
4318
|
+
}
|
|
4319
|
+
/**
|
|
4320
|
+
* Sends the user's message and returns the assistant's reply text —
|
|
4321
|
+
* the ergonomic path. Use {@link sendTurn} when you also need the
|
|
4322
|
+
* per-turn cache statistics or the raw response.
|
|
4323
|
+
*/
|
|
4324
|
+
async send(content, overrides) {
|
|
4325
|
+
const turn = await this.sendTurn(content, overrides);
|
|
4326
|
+
return turn.content;
|
|
4327
|
+
}
|
|
4328
|
+
/**
|
|
4329
|
+
* Sends the user's message and returns the full turn record: reply text,
|
|
4330
|
+
* per-turn KV-cache statistics, and the raw `/api/chat` response.
|
|
4331
|
+
*/
|
|
4332
|
+
async sendTurn(content, overrides) {
|
|
4333
|
+
this.history.push(Object.freeze({ role: "user", content }));
|
|
4334
|
+
const response = await this.client.chat({
|
|
4335
|
+
model: this.config.model,
|
|
4336
|
+
messages: [...this.history],
|
|
4337
|
+
...this.config.tools !== void 0 || overrides?.tools !== void 0 ? { tools: overrides?.tools ?? this.config.tools } : {},
|
|
4338
|
+
...this.config.format !== void 0 || overrides?.format !== void 0 ? { format: overrides?.format ?? this.config.format } : {},
|
|
4339
|
+
options: { ...this.config.options, ...overrides?.options },
|
|
4340
|
+
...this.config.think !== void 0 || overrides?.think !== void 0 ? { think: overrides?.think ?? this.config.think } : {},
|
|
4341
|
+
...this.config.keep_alive !== void 0 || overrides?.keep_alive !== void 0 ? { keep_alive: overrides?.keep_alive ?? this.config.keep_alive } : {},
|
|
4342
|
+
...overrides?.signal !== void 0 ? { signal: overrides.signal } : {},
|
|
4343
|
+
...overrides?.timeoutMs !== void 0 ? { timeoutMs: overrides.timeoutMs } : {},
|
|
4344
|
+
stream: false
|
|
4345
|
+
});
|
|
4346
|
+
this.history.push(Object.freeze({ ...response.message }));
|
|
4347
|
+
const cache = extractTurnStats(response);
|
|
4348
|
+
this.tallies.turns += 1;
|
|
4349
|
+
this.tallies.cachedTokens += cache.cachedTokens;
|
|
4350
|
+
this.tallies.evaluatedTokens += cache.evaluatedTokens;
|
|
4351
|
+
return { content: response.message.content, cache, response };
|
|
4352
|
+
}
|
|
4353
|
+
/**
|
|
4354
|
+
* A defensive copy of the session history — mutating the returned array or
|
|
4355
|
+
* its frozen messages has no effect on the session.
|
|
4356
|
+
*/
|
|
4357
|
+
getMessages() {
|
|
4358
|
+
return this.history.map((message) => message);
|
|
4359
|
+
}
|
|
4360
|
+
/**
|
|
4361
|
+
* Clears the conversation back to its initial state (system prompt, zero
|
|
4362
|
+
* turns, zeroed cache statistics). The next turn starts with a cold cache
|
|
4363
|
+
* by definition — there is nothing upstream of it to reuse.
|
|
4364
|
+
*/
|
|
4365
|
+
reset() {
|
|
4366
|
+
this.history.length = 0;
|
|
4367
|
+
if (this.config.systemPrompt !== void 0 && this.config.systemPrompt !== "") {
|
|
4368
|
+
this.history.push(
|
|
4369
|
+
Object.freeze({ role: "system", content: this.config.systemPrompt })
|
|
4370
|
+
);
|
|
4371
|
+
}
|
|
4372
|
+
this.tallies.turns = 0;
|
|
4373
|
+
this.tallies.cachedTokens = 0;
|
|
4374
|
+
this.tallies.evaluatedTokens = 0;
|
|
4375
|
+
}
|
|
4376
|
+
};
|
|
4377
|
+
|
|
4378
|
+
// src/context-safety.ts
|
|
4379
|
+
var DEFAULT_CONTEXT_WARNING_THRESHOLD = 0.9;
|
|
4380
|
+
var IMAGE_TOKEN_ESTIMATE = 300;
|
|
4381
|
+
var CHARS_PER_TOKEN = 4;
|
|
4382
|
+
function isCjkCodePoint(code) {
|
|
4383
|
+
return code >= 19968 && code <= 40959 || // CJK Unified Ideographs
|
|
4384
|
+
code >= 13312 && code <= 19903 || // CJK Extension A
|
|
4385
|
+
code >= 12352 && code <= 12543 || // Hiragana + Katakana
|
|
4386
|
+
code >= 44032 && code <= 55215 || // Hangul syllables
|
|
4387
|
+
code >= 63744 && code <= 64255 || // CJK Compatibility Ideographs
|
|
4388
|
+
code >= 65280 && code <= 65519;
|
|
4389
|
+
}
|
|
4390
|
+
function estimateTokens(text) {
|
|
4391
|
+
if (text.length === 0) return 0;
|
|
4392
|
+
let cjk = 0;
|
|
4393
|
+
let other = 0;
|
|
4394
|
+
for (const char of text) {
|
|
4395
|
+
if (isCjkCodePoint(char.codePointAt(0) ?? 0)) cjk += 1;
|
|
4396
|
+
else other += 1;
|
|
4397
|
+
}
|
|
4398
|
+
return Math.ceil(cjk + other / CHARS_PER_TOKEN);
|
|
4399
|
+
}
|
|
4400
|
+
var MESSAGE_OVERHEAD_TOKENS = 4;
|
|
4401
|
+
function estimateMessageTokens(message) {
|
|
4402
|
+
let total = MESSAGE_OVERHEAD_TOKENS + estimateTokens(message.content ?? "");
|
|
4403
|
+
if (message.thinking !== void 0) total += estimateTokens(message.thinking);
|
|
4404
|
+
if (message.tool_calls !== void 0) {
|
|
4405
|
+
for (const call of message.tool_calls) {
|
|
4406
|
+
total += estimateTokens(call.function.name);
|
|
4407
|
+
total += estimateTokens(JSON.stringify(call.function.arguments ?? {}));
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
if (message.images !== void 0) total += message.images.length * IMAGE_TOKEN_ESTIMATE;
|
|
4411
|
+
return total;
|
|
4412
|
+
}
|
|
4413
|
+
function estimateChatRequestTokens(req) {
|
|
4414
|
+
let total = 0;
|
|
4415
|
+
for (const message of req.messages) total += estimateMessageTokens(message);
|
|
4416
|
+
if (req.tools !== void 0) {
|
|
4417
|
+
for (const tool of req.tools) {
|
|
4418
|
+
total += estimateTokens(tool.function.description ?? "");
|
|
4419
|
+
total += estimateTokens(JSON.stringify(tool.function.parameters ?? {}));
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
return total;
|
|
4423
|
+
}
|
|
4424
|
+
function estimateGenerateRequestTokens(req) {
|
|
4425
|
+
let total = estimateTokens(req.prompt ?? "");
|
|
4426
|
+
if (req.system !== void 0) total += estimateTokens(req.system);
|
|
4427
|
+
if (req.template !== void 0) total += estimateTokens(req.template);
|
|
4428
|
+
if (req.images !== void 0) total += req.images.length * IMAGE_TOKEN_ESTIMATE;
|
|
4429
|
+
return total;
|
|
4430
|
+
}
|
|
4431
|
+
function checkChatContext(req, options = {}) {
|
|
4432
|
+
return check(estimateChatRequestTokens(req), req.options?.num_ctx, options);
|
|
4433
|
+
}
|
|
4434
|
+
function checkGenerateContext(req, options = {}) {
|
|
4435
|
+
return check(estimateGenerateRequestTokens(req), req.options?.num_ctx, options);
|
|
4436
|
+
}
|
|
4437
|
+
function check(estimatedPromptTokens, requestNumCtx, options) {
|
|
4438
|
+
const threshold = options.threshold ?? DEFAULT_CONTEXT_WARNING_THRESHOLD;
|
|
4439
|
+
if (requestNumCtx !== void 0) {
|
|
4440
|
+
return {
|
|
4441
|
+
estimatedPromptTokens,
|
|
4442
|
+
effectiveContextLength: requestNumCtx,
|
|
4443
|
+
windowSource: "request",
|
|
4444
|
+
exceedsThreshold: estimatedPromptTokens > requestNumCtx * threshold
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
if (options.defaultContextLength !== void 0) {
|
|
4448
|
+
return {
|
|
4449
|
+
estimatedPromptTokens,
|
|
4450
|
+
effectiveContextLength: options.defaultContextLength,
|
|
4451
|
+
windowSource: "client-default",
|
|
4452
|
+
exceedsThreshold: estimatedPromptTokens > options.defaultContextLength * threshold
|
|
4453
|
+
};
|
|
4454
|
+
}
|
|
4455
|
+
return {
|
|
4456
|
+
estimatedPromptTokens,
|
|
4457
|
+
effectiveContextLength: OLLAMA_FALLBACK_CONTEXT_LENGTH,
|
|
4458
|
+
windowSource: "server-fallback",
|
|
4459
|
+
exceedsThreshold: estimatedPromptTokens > OLLAMA_FALLBACK_CONTEXT_LENGTH
|
|
4460
|
+
};
|
|
4461
|
+
}
|
|
4462
|
+
var OLLAMA_FALLBACK_CONTEXT_LENGTH = 2048;
|
|
4463
|
+
function contextWarningMessage(check2, kind) {
|
|
4464
|
+
const where = `OllamaClient.${kind}()`;
|
|
4465
|
+
const estimate = `~${check2.estimatedPromptTokens} tokens`;
|
|
4466
|
+
if (check2.windowSource === "request") {
|
|
4467
|
+
return `${where}: estimated prompt size ${estimate} is close to or beyond the request's context window (num_ctx=${check2.effectiveContextLength}). The server will likely silently truncate the top of the conversation \u2014 the model loses earlier turns with no error. Raise \`options.num_ctx\` or shorten the conversation. Estimates are heuristic (\xB120\u201330%); see \`estimateTokens\`.`;
|
|
4468
|
+
}
|
|
4469
|
+
if (check2.windowSource === "client-default") {
|
|
4470
|
+
return `${where}: estimated prompt size ${estimate} is close to or beyond the client's \`defaultContextLength\` (${check2.effectiveContextLength}, injected as \`num_ctx\`). The server will likely silently truncate the top of the conversation \u2014 the model loses earlier turns with no error. Raise \`defaultContextLength\`, set \`options.num_ctx\` per request, or shorten the conversation. Estimates are heuristic (\xB120\u201330%); see \`estimateTokens\`.`;
|
|
4471
|
+
}
|
|
4472
|
+
return `${where}: estimated prompt size ${estimate} outright exceeds Ollama's conservative server-default context window (${OLLAMA_FALLBACK_CONTEXT_LENGTH}\u20134096 tokens when \`num_ctx\` is unset \u2014 model-dependent). Unless the model's default is larger, the server will silently truncate the top of the conversation \u2014 the model loses earlier turns with no error. Set \`options.num_ctx\` (or the client's \`defaultContextLength\`) to an explicit window to make truncation visible and controllable. Estimates are heuristic (\xB120\u201330%); see \`estimateTokens\`.`;
|
|
4473
|
+
}
|
|
3405
4474
|
|
|
3406
4475
|
// src/generated/api/operations.ts
|
|
3407
4476
|
var blobsOp = {
|
|
@@ -3613,7 +4682,6 @@ var NativeApi = class {
|
|
|
3613
4682
|
constructor(runtime) {
|
|
3614
4683
|
this.runtime = runtime;
|
|
3615
4684
|
}
|
|
3616
|
-
runtime;
|
|
3617
4685
|
blobs(digest, options) {
|
|
3618
4686
|
return this.runtime.invoke({
|
|
3619
4687
|
operation: blobsOp,
|
|
@@ -3716,7 +4784,6 @@ var FailoverHttpClient = class {
|
|
|
3716
4784
|
this.client = client;
|
|
3717
4785
|
this.baseUrl = baseUrl;
|
|
3718
4786
|
}
|
|
3719
|
-
client;
|
|
3720
4787
|
/**
|
|
3721
4788
|
* The first candidate endpoint's base URL. Used for inferRuntimeMode()
|
|
3722
4789
|
* checks (local vs cloud). The actual request routing picks the best
|
|
@@ -3923,14 +4990,21 @@ var OllamaClient = class {
|
|
|
3923
4990
|
* `OLLAMA_API_KEY`), independent of whether `config.endpoints` was used instead.
|
|
3924
4991
|
*/
|
|
3925
4992
|
cloudApiKey;
|
|
4993
|
+
/** Configured `defaultContextLength` — injected as `num_ctx` when a request omits it. */
|
|
4994
|
+
defaultContextLength;
|
|
4995
|
+
/** Configured `contextWarningThreshold` (default 0.9). */
|
|
4996
|
+
contextWarningThreshold;
|
|
4997
|
+
/** Configured overflow policy — `'warn'` (default) or `'throw'`. */
|
|
4998
|
+
onContextOverflow;
|
|
3926
4999
|
constructor(config = {}) {
|
|
3927
|
-
|
|
3928
|
-
|
|
5000
|
+
this.cloudApiKey = resolveApiKey(config.apiKey);
|
|
5001
|
+
const defaultBaseUrl = resolveBaseUrl(config.baseUrl);
|
|
5002
|
+
const resolvedApiKey = resolveEndpointApiKey(config.apiKey, defaultBaseUrl);
|
|
3929
5003
|
const credentialEndpoints = resolveCredentialEndpoints(config);
|
|
3930
5004
|
const endpoints = config.endpoints ?? (credentialEndpoints.length > 0 ? [] : [
|
|
3931
5005
|
{
|
|
3932
5006
|
name: "default",
|
|
3933
|
-
baseUrl:
|
|
5007
|
+
baseUrl: defaultBaseUrl,
|
|
3934
5008
|
...resolvedApiKey !== void 0 ? { apiKey: resolvedApiKey } : {},
|
|
3935
5009
|
...config.headers !== void 0 ? { headers: config.headers } : {}
|
|
3936
5010
|
}
|
|
@@ -3946,6 +5020,9 @@ var OllamaClient = class {
|
|
|
3946
5020
|
this.middleware = config.middleware;
|
|
3947
5021
|
this.onLifecycleEvent = config.onLifecycleEvent;
|
|
3948
5022
|
this.retryConfig = typeof config.retries === "number" ? { ...DEFAULT_RETRY_CONFIG, maxRetries: config.retries } : { ...DEFAULT_RETRY_CONFIG, ...config.retries };
|
|
5023
|
+
this.defaultContextLength = config.defaultContextLength;
|
|
5024
|
+
this.contextWarningThreshold = config.contextWarningThreshold;
|
|
5025
|
+
this.onContextOverflow = config.onContextOverflow ?? "warn";
|
|
3949
5026
|
this.models = new ModelsClient((op, opts) => this.executeWithFailover(op, opts));
|
|
3950
5027
|
}
|
|
3951
5028
|
get modelsClient() {
|
|
@@ -4035,6 +5112,54 @@ var OllamaClient = class {
|
|
|
4035
5112
|
return this._decision;
|
|
4036
5113
|
}
|
|
4037
5114
|
_decision;
|
|
5115
|
+
/**
|
|
5116
|
+
* Ergonomic OpenAI Responses API bridge (`client.responses.create()` /
|
|
5117
|
+
* `.stream()`). Dual-mode transport: prefers the native `POST
|
|
5118
|
+
* /v1/responses` endpoint (Ollama ≥ v0.13.3) and transparently re-issues via
|
|
5119
|
+
* `/api/chat` when the server answers 404, so migrating OpenAI code works
|
|
5120
|
+
* unchanged against older servers. See `src/responses.ts`.
|
|
5121
|
+
*/
|
|
5122
|
+
get responses() {
|
|
5123
|
+
if (this._responses === void 0) {
|
|
5124
|
+
this._responses = new ResponsesModule((op, opts) => this.executeWithFailover(op, opts));
|
|
5125
|
+
}
|
|
5126
|
+
return this._responses;
|
|
5127
|
+
}
|
|
5128
|
+
_responses;
|
|
5129
|
+
session(modelOrOptions, systemPrompt) {
|
|
5130
|
+
const options = typeof modelOrOptions === "string" ? {
|
|
5131
|
+
model: modelOrOptions,
|
|
5132
|
+
...systemPrompt !== void 0 ? { systemPrompt } : {}
|
|
5133
|
+
} : modelOrOptions;
|
|
5134
|
+
return new ConversationSession(this, options);
|
|
5135
|
+
}
|
|
5136
|
+
/**
|
|
5137
|
+
* Context-window pre-flight enforcement — see `src/context-safety.ts` and
|
|
5138
|
+
* the `defaultContextLength` / `onContextOverflow` config docs. Logs a
|
|
5139
|
+
* warning (default) or throws client-side when the estimated prompt size
|
|
5140
|
+
* is close to / beyond the effective context window.
|
|
5141
|
+
*/
|
|
5142
|
+
enforceContextSafety(kind, check2) {
|
|
5143
|
+
if (!check2.exceedsThreshold) return;
|
|
5144
|
+
const message = contextWarningMessage(check2, kind);
|
|
5145
|
+
if (this.onContextOverflow === "throw") {
|
|
5146
|
+
throw new OllamaClientError(message, { code: "context_overflow" });
|
|
5147
|
+
}
|
|
5148
|
+
this.logger.warn(message);
|
|
5149
|
+
}
|
|
5150
|
+
/**
|
|
5151
|
+
* Returns `options` with `num_ctx` injected from the client's
|
|
5152
|
+
* `defaultContextLength` when the request didn't set one — making the
|
|
5153
|
+
* effective context window explicit on the wire instead of relying on
|
|
5154
|
+
* Ollama's silently-truncating server default. Returns the original
|
|
5155
|
+
* reference when there's nothing to inject (the common case).
|
|
5156
|
+
*/
|
|
5157
|
+
injectDefaultContextLength(options) {
|
|
5158
|
+
if (options?.num_ctx !== void 0 || this.defaultContextLength === void 0) {
|
|
5159
|
+
return options;
|
|
5160
|
+
}
|
|
5161
|
+
return { ...options, num_ctx: this.defaultContextLength };
|
|
5162
|
+
}
|
|
4038
5163
|
/**
|
|
4039
5164
|
* Fail-fast guard for `format` (structured output) requests: throws before any network
|
|
4040
5165
|
* call if the candidate endpoint is inferred as Ollama Cloud, which does not currently
|
|
@@ -4144,7 +5269,7 @@ var OllamaClient = class {
|
|
|
4144
5269
|
}
|
|
4145
5270
|
}
|
|
4146
5271
|
async chat(req) {
|
|
4147
|
-
const encodedMessages = await withEncodedMessageImages(req.messages);
|
|
5272
|
+
const encodedMessages = await withEncodedMessageImages(req.messages, req.signal);
|
|
4148
5273
|
const messages = encodedMessages.map((message) => {
|
|
4149
5274
|
if (message.role === "tool" && message.tool_call_id !== void 0) {
|
|
4150
5275
|
const { tool_call_id: _toolCallId, ...nativeMessage } = message;
|
|
@@ -4152,6 +5277,14 @@ var OllamaClient = class {
|
|
|
4152
5277
|
}
|
|
4153
5278
|
return message;
|
|
4154
5279
|
});
|
|
5280
|
+
this.enforceContextSafety(
|
|
5281
|
+
"chat",
|
|
5282
|
+
checkChatContext(req, {
|
|
5283
|
+
defaultContextLength: this.defaultContextLength,
|
|
5284
|
+
threshold: this.contextWarningThreshold
|
|
5285
|
+
})
|
|
5286
|
+
);
|
|
5287
|
+
const options = this.injectDefaultContextLength(req.options);
|
|
4155
5288
|
if (req.stream) {
|
|
4156
5289
|
return this.executeWithFailover(
|
|
4157
5290
|
async (http, signal) => {
|
|
@@ -4159,7 +5292,7 @@ var OllamaClient = class {
|
|
|
4159
5292
|
this.assertStructuredOutputSupported(http.baseUrl, req.model);
|
|
4160
5293
|
const stream = await http.requestStream({
|
|
4161
5294
|
path: "/api/chat",
|
|
4162
|
-
body: { ...req, messages, stream: true },
|
|
5295
|
+
body: { ...req, options, messages, stream: true },
|
|
4163
5296
|
signal
|
|
4164
5297
|
});
|
|
4165
5298
|
return normalizeChatStream(stream, signal);
|
|
@@ -4188,7 +5321,7 @@ var OllamaClient = class {
|
|
|
4188
5321
|
this.assertStructuredOutputSupported(http.baseUrl, req.model);
|
|
4189
5322
|
return http.request({
|
|
4190
5323
|
path: "/api/chat",
|
|
4191
|
-
body: { ...req, messages, stream: false },
|
|
5324
|
+
body: { ...req, options, messages, stream: false },
|
|
4192
5325
|
signal
|
|
4193
5326
|
});
|
|
4194
5327
|
}, req);
|
|
@@ -4215,7 +5348,15 @@ var OllamaClient = class {
|
|
|
4215
5348
|
return parseStructuredOutput(res.message.content, schema);
|
|
4216
5349
|
}
|
|
4217
5350
|
async generate(req) {
|
|
4218
|
-
const encodedReq = await withEncodedImages(req);
|
|
5351
|
+
const encodedReq = await withEncodedImages(req, req.signal);
|
|
5352
|
+
this.enforceContextSafety(
|
|
5353
|
+
"generate",
|
|
5354
|
+
checkGenerateContext(req, {
|
|
5355
|
+
defaultContextLength: this.defaultContextLength,
|
|
5356
|
+
threshold: this.contextWarningThreshold
|
|
5357
|
+
})
|
|
5358
|
+
);
|
|
5359
|
+
const options = this.injectDefaultContextLength(req.options);
|
|
4219
5360
|
if (encodedReq.stream) {
|
|
4220
5361
|
return this.executeWithFailover(
|
|
4221
5362
|
async (http, signal) => {
|
|
@@ -4223,7 +5364,7 @@ var OllamaClient = class {
|
|
|
4223
5364
|
this.assertStructuredOutputSupported(http.baseUrl, encodedReq.model);
|
|
4224
5365
|
const stream = await http.requestStream({
|
|
4225
5366
|
path: "/api/generate",
|
|
4226
|
-
body: { ...encodedReq, stream: true },
|
|
5367
|
+
body: { ...encodedReq, options, stream: true },
|
|
4227
5368
|
signal
|
|
4228
5369
|
});
|
|
4229
5370
|
return normalizeGenerateStream(stream, signal);
|
|
@@ -4249,7 +5390,7 @@ var OllamaClient = class {
|
|
|
4249
5390
|
this.assertStructuredOutputSupported(http.baseUrl, encodedReq.model);
|
|
4250
5391
|
return http.request({
|
|
4251
5392
|
path: "/api/generate",
|
|
4252
|
-
body: { ...encodedReq, stream: false },
|
|
5393
|
+
body: { ...encodedReq, options, stream: false },
|
|
4253
5394
|
signal
|
|
4254
5395
|
});
|
|
4255
5396
|
}, encodedReq);
|
|
@@ -4307,6 +5448,18 @@ var OllamaClient = class {
|
|
|
4307
5448
|
version = () => this.models.version();
|
|
4308
5449
|
createBlob = (digest, data) => this.models.createBlob(digest, data);
|
|
4309
5450
|
checkBlob = (digest) => this.models.checkBlob(digest);
|
|
5451
|
+
/**
|
|
5452
|
+
* Immediately unloads `model` from VRAM. Convenience alias for
|
|
5453
|
+
* {@link ModelsClient.unload} — see that method for the full
|
|
5454
|
+
* lifecycle contract.
|
|
5455
|
+
*/
|
|
5456
|
+
unloadModel = (model) => this.models.unload(model);
|
|
5457
|
+
/**
|
|
5458
|
+
* Pre-loads and pins `model` into VRAM indefinitely. Convenience
|
|
5459
|
+
* alias for {@link ModelsClient.pin} — see that method for the
|
|
5460
|
+
* full lifecycle contract.
|
|
5461
|
+
*/
|
|
5462
|
+
pinModel = (model) => this.models.pin(model);
|
|
4310
5463
|
// --- System One (Wave 13) ---
|
|
4311
5464
|
/**
|
|
4312
5465
|
* Ollama's System One decision layer (`POST /v1/systemone`) — evaluates
|
|
@@ -4376,6 +5529,63 @@ var OllamaClient = class {
|
|
|
4376
5529
|
req
|
|
4377
5530
|
);
|
|
4378
5531
|
}
|
|
5532
|
+
/**
|
|
5533
|
+
* Ollama Cloud usage statistics (`GET https://ollama.com/api/usage`) — request counts,
|
|
5534
|
+
* USD spend, and token totals (including cached input tokens) for cloud inference,
|
|
5535
|
+
* web search, and web fetch, bucketed by hour (`range: '24h'`) or day (`'7d'`/`'30d'`).
|
|
5536
|
+
*
|
|
5537
|
+
* See {@link OllamaClient.webSearch} for the cloud-endpoint/auth/timeout/retry
|
|
5538
|
+
* behavior, which applies identically here. Query parameters mirror the official
|
|
5539
|
+
* endpoint: `range` (`'24h' | '7d' | '30d'`, server default `'7d'`) and `scope`
|
|
5540
|
+
* (`'self' | 'team'`, server default `'self'`; team scope requires a team admin).
|
|
5541
|
+
* Omitted options are left unset so the server applies its own defaults.
|
|
5542
|
+
*
|
|
5543
|
+
* ```ts
|
|
5544
|
+
* const usage = await client.usage({ range: '24h' });
|
|
5545
|
+
* console.log(usage.totals.request_count, usage.totals.usage_usd);
|
|
5546
|
+
* for (const bucket of usage.buckets) {
|
|
5547
|
+
* if (bucket.partial) continue; // current hour, still in progress
|
|
5548
|
+
* // ...
|
|
5549
|
+
* }
|
|
5550
|
+
* ```
|
|
5551
|
+
*/
|
|
5552
|
+
usage(req = {}) {
|
|
5553
|
+
const params = new URLSearchParams();
|
|
5554
|
+
if (req.range !== void 0) params.set("range", req.range);
|
|
5555
|
+
if (req.scope !== void 0) params.set("scope", req.scope);
|
|
5556
|
+
const query = params.toString();
|
|
5557
|
+
return this.executeCloudRequest(
|
|
5558
|
+
(http, signal) => http.request({
|
|
5559
|
+
path: query ? `/api/usage?${query}` : "/api/usage",
|
|
5560
|
+
method: "GET",
|
|
5561
|
+
signal
|
|
5562
|
+
}),
|
|
5563
|
+
req
|
|
5564
|
+
);
|
|
5565
|
+
}
|
|
5566
|
+
/**
|
|
5567
|
+
* Ollama Cloud balance (`GET https://ollama.com/api/balance`) — remaining included
|
|
5568
|
+
* and purchased usage credits. The included balance is either a plan-period credit
|
|
5569
|
+
* object (`balance_usd`/`allowance_usd`/`period`) or, on legacy plans, session/weekly
|
|
5570
|
+
* percentage limits; the shape is discriminated at runtime by the server response.
|
|
5571
|
+
* See {@link OllamaClient.webSearch} for the cloud-endpoint/auth/timeout/retry
|
|
5572
|
+
* behavior, which applies identically here.
|
|
5573
|
+
*
|
|
5574
|
+
* ```ts
|
|
5575
|
+
* const balance = await client.balance();
|
|
5576
|
+
* if ('balance_usd' in balance.included) {
|
|
5577
|
+
* console.log(balance.included.balance_usd, balance.purchased.balance_usd);
|
|
5578
|
+
* } else {
|
|
5579
|
+
* console.log(balance.included.session.remaining_percent, '% of session limit left');
|
|
5580
|
+
* }
|
|
5581
|
+
* ```
|
|
5582
|
+
*/
|
|
5583
|
+
balance(req = {}) {
|
|
5584
|
+
return this.executeCloudRequest(
|
|
5585
|
+
(http, signal) => http.request({ path: "/api/balance", method: "GET", signal }),
|
|
5586
|
+
req
|
|
5587
|
+
);
|
|
5588
|
+
}
|
|
4379
5589
|
/**
|
|
4380
5590
|
* Runs `operation` against the fixed Ollama Cloud host with this client's default
|
|
4381
5591
|
* timeout and retry policy — the same primitives `executeWithFailover` uses, minus the
|
|
@@ -4479,6 +5689,91 @@ var OllamaClient = class {
|
|
|
4479
5689
|
}
|
|
4480
5690
|
};
|
|
4481
5691
|
|
|
5692
|
+
// src/transport/fetch-with-backoff.ts
|
|
5693
|
+
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 503]);
|
|
5694
|
+
var DEFAULT_FETCH_BACKOFF_CONFIG = {
|
|
5695
|
+
maxRetries: 3
|
|
5696
|
+
};
|
|
5697
|
+
function sleep2(ms, signal) {
|
|
5698
|
+
return new Promise((resolve, reject) => {
|
|
5699
|
+
if (signal?.aborted) {
|
|
5700
|
+
reject(
|
|
5701
|
+
Object.assign(new Error("fetchWithBackoff aborted during backoff sleep"), {
|
|
5702
|
+
name: "AbortError"
|
|
5703
|
+
})
|
|
5704
|
+
);
|
|
5705
|
+
return;
|
|
5706
|
+
}
|
|
5707
|
+
const timer = setTimeout(() => {
|
|
5708
|
+
signal?.removeEventListener("abort", onAbort);
|
|
5709
|
+
resolve();
|
|
5710
|
+
}, ms);
|
|
5711
|
+
const onAbort = () => {
|
|
5712
|
+
clearTimeout(timer);
|
|
5713
|
+
signal?.removeEventListener("abort", onAbort);
|
|
5714
|
+
reject(
|
|
5715
|
+
Object.assign(new Error("fetchWithBackoff aborted during backoff sleep"), {
|
|
5716
|
+
name: "AbortError"
|
|
5717
|
+
})
|
|
5718
|
+
);
|
|
5719
|
+
};
|
|
5720
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
5721
|
+
});
|
|
5722
|
+
}
|
|
5723
|
+
async function fetchWithBackoff(input, init, config = {}) {
|
|
5724
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
5725
|
+
if (typeof fetchImpl !== "function") {
|
|
5726
|
+
throw new Error(
|
|
5727
|
+
"fetchWithBackoff: no global fetch is available. Pass a fetch implementation via config.fetch, or run in Node.js >= 18 / a browser-like environment."
|
|
5728
|
+
);
|
|
5729
|
+
}
|
|
5730
|
+
const maxRetries = config.maxRetries ?? DEFAULT_FETCH_BACKOFF_CONFIG.maxRetries;
|
|
5731
|
+
const backoff = config.backoff ?? DEFAULT_BACKOFF;
|
|
5732
|
+
const signal = config.signal;
|
|
5733
|
+
let lastError;
|
|
5734
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
5735
|
+
if (signal?.aborted) {
|
|
5736
|
+
throw Object.assign(new Error("fetchWithBackoff aborted"), { name: "AbortError" });
|
|
5737
|
+
}
|
|
5738
|
+
try {
|
|
5739
|
+
const response = await fetchImpl(input, {
|
|
5740
|
+
...init,
|
|
5741
|
+
...signal !== void 0 ? { signal } : {}
|
|
5742
|
+
});
|
|
5743
|
+
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < maxRetries) {
|
|
5744
|
+
const delayMs = calculateBackoff(attempt, backoff);
|
|
5745
|
+
config.onRetry?.(attempt, response.status, delayMs);
|
|
5746
|
+
await sleep2(delayMs, signal);
|
|
5747
|
+
continue;
|
|
5748
|
+
}
|
|
5749
|
+
return response;
|
|
5750
|
+
} catch (err) {
|
|
5751
|
+
lastError = err;
|
|
5752
|
+
if (signal?.aborted || err instanceof Error && err.name === "AbortError") {
|
|
5753
|
+
throw err;
|
|
5754
|
+
}
|
|
5755
|
+
if (attempt < maxRetries) {
|
|
5756
|
+
const delayMs = calculateBackoff(attempt, backoff);
|
|
5757
|
+
config.onRetry?.(attempt, 0, delayMs);
|
|
5758
|
+
await sleep2(delayMs, signal);
|
|
5759
|
+
continue;
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
throw lastError ?? new Error("fetchWithBackoff: exhausted retry attempts");
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5766
|
+
// src/done-reason.ts
|
|
5767
|
+
var KNOWN_DONE_REASONS = /* @__PURE__ */ new Set([
|
|
5768
|
+
"stop",
|
|
5769
|
+
"length",
|
|
5770
|
+
"load",
|
|
5771
|
+
"unload"
|
|
5772
|
+
]);
|
|
5773
|
+
function isKnownDoneReason(value) {
|
|
5774
|
+
return value !== void 0 && value !== null && KNOWN_DONE_REASONS.has(value);
|
|
5775
|
+
}
|
|
5776
|
+
|
|
4482
5777
|
// src/streaming/adapters.ts
|
|
4483
5778
|
function toTextStream(stream) {
|
|
4484
5779
|
const iterator = stream[Symbol.asyncIterator]();
|
|
@@ -4934,9 +6229,9 @@ function validate(value, schema, path) {
|
|
|
4934
6229
|
["exclusiveMinimum", (n, b) => n > b, "must be greater than"],
|
|
4935
6230
|
["exclusiveMaximum", (n, b) => n < b, "must be less than"]
|
|
4936
6231
|
];
|
|
4937
|
-
for (const [keyword,
|
|
6232
|
+
for (const [keyword, check2, phrase] of numericRules) {
|
|
4938
6233
|
const bound = s[keyword];
|
|
4939
|
-
if (typeof bound === "number" && !
|
|
6234
|
+
if (typeof bound === "number" && !check2(value, bound)) issues.push({ path, message: label(path) + " " + phrase + " " + bound });
|
|
4940
6235
|
}
|
|
4941
6236
|
const multipleOf = s["multipleOf"];
|
|
4942
6237
|
if (typeof multipleOf === "number" && multipleOf > 0) {
|
|
@@ -5197,8 +6492,6 @@ var McpBridge = class _McpBridge {
|
|
|
5197
6492
|
this.client = client;
|
|
5198
6493
|
this.options = options;
|
|
5199
6494
|
}
|
|
5200
|
-
client;
|
|
5201
|
-
options;
|
|
5202
6495
|
static toOllamaTools(tools, options = {}) {
|
|
5203
6496
|
const prefix = options.namePrefix ?? "";
|
|
5204
6497
|
return tools.map((tool) => ({
|
|
@@ -5495,6 +6788,13 @@ var Agent = class {
|
|
|
5495
6788
|
}
|
|
5496
6789
|
};
|
|
5497
6790
|
|
|
5498
|
-
|
|
6791
|
+
// src/system-one.ts
|
|
6792
|
+
var MAX_SYSTEM_ONE_IMAGES_BYTES = 32 * 1024 * 1024;
|
|
6793
|
+
var MAX_SYSTEM_ONE_REQUEST_BYTES = 64 * 1024;
|
|
6794
|
+
function estimateSystemOneRequestBytes(request) {
|
|
6795
|
+
return new TextEncoder().encode(JSON.stringify(request)).byteLength;
|
|
6796
|
+
}
|
|
6797
|
+
|
|
6798
|
+
export { ATTR_GEN_AI_OPERATION_NAME, ATTR_GEN_AI_REQUEST_MODEL, ATTR_GEN_AI_RESPONSE_MODEL, ATTR_GEN_AI_SYSTEM, ATTR_GEN_AI_TOOL_CALL_ID, ATTR_GEN_AI_TOOL_NAME, ATTR_GEN_AI_USAGE_INPUT_TOKENS, ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_OLLAMA_AGENT_ITERATION, ATTR_OLLAMA_AGENT_MAX_ITERATIONS, ATTR_OLLAMA_ENDPOINT_ATTEMPT, ATTR_OLLAMA_ENDPOINT_NAME, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL, Agent, AnthropicCompatClient, AnthropicMessagesStream, ConversationSession, DEFAULT_BACKOFF, DEFAULT_BASE_URL, DEFAULT_CONTEXT_WARNING_THRESHOLD, DEFAULT_FAILOVER_CODES, DEFAULT_FETCH_BACKOFF_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TIMEOUT_MS, EndpointRegistry, GEN_AI_SYSTEM_OLLAMA, HttpClient, IMAGE_TOKEN_ESTIMATE, KEEP_ALIVE_INDEFINITE, KEEP_ALIVE_UNLOAD, KNOWN_DONE_REASONS, MAX_SYSTEM_ONE_IMAGES_BYTES, MAX_SYSTEM_ONE_REQUEST_BYTES, McpBridge, ModelsClient, NANOS_PER_MS, NANOS_PER_SECOND, NOOP_LOGGER, OLLAMA_CLOUD_BASE_URL, OLLAMA_FALLBACK_CONTEXT_LENGTH, OllamaAbortError, OllamaAgentMaxIterationsError, OllamaAgentMaxToolCallsError, OllamaAuthError, OllamaBadGatewayError, OllamaClient, OllamaClientError, OllamaGenericClientError, OllamaIncompatibleModelError, OllamaMcpError, OllamaModelRoutingError, OllamaNetworkError, OllamaNotFoundError, OllamaOverloadedError, OllamaQuotaExceededError, OllamaRateLimitError, OllamaRequestTooLargeError, OllamaRequestValidationError, OllamaResponseValidationError, OllamaServerError, OllamaServerVersionUnknownError, OllamaSkillInvalidError, OllamaSkillNotFoundError, OllamaStream, OllamaStreamError, OllamaTimeoutError, OllamaToolTimeoutError, OllamaToolValidationError, OllamaUnsupportedCapabilityError, OpenAIChatCompletionStream, OpenAICompatClient, OpenAICompletionStream, OpenAIResponsesStream, OpenAIResponsesStreamError, QuotaManager, RETRYABLE_STATUS_CODES, ResponsesModule, ToolCallAccumulator, ToolRegistry, addSpanEvent, applySkill, calculateBackoff, checkChatContext, checkEndpointHealth, checkGenerateContext, composeMiddleware, contextWarningMessage, createConsoleLogger, createDecision, createOllamaCloudFreeTierQuota, createTimeoutSignal, defineTool, detectModelCapabilities, encodeImage, estimateChatRequestTokens, estimateGenerateRequestTokens, estimateSystemOneRequestBytes, estimateTokens, extractUsage, fetchWithBackoff, imageStringNeedsResolution, inferRuntimeMode, isKeepAliveSugar, isKnownDoneReason, isSameToolCall, listAllMcpTools, listAvailableModels, loadMcpTools, mapError, mergeToolCall, mergeToolCallArgumentsString, mergeToolCallArrays, normalizeChatStream, normalizeGenerateStream, normalizeKeepAlive, normalizeProgressStream, parseFrontmatter, parseNdjsonStream, parseSseStream, parseStructuredOutput, registerMcpTools, resolveCredentialEndpoints, resolveImageInput, setSpanError, toDataStream, toResponse, toTextStream, withRetry, withSpan, zodToJsonSchema };
|
|
5499
6799
|
//# sourceMappingURL=index.js.map
|
|
5500
6800
|
//# sourceMappingURL=index.js.map
|