@nemesis-oss/ollama-sdk 1.11.0 → 1.12.1
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 +37 -0
- package/README.md +131 -0
- package/dist/{anthropic-CfricAhK.d.ts → anthropic-CKp5TmAP.d.ts} +1 -1
- package/dist/{anthropic-DinrABof.d.cts → anthropic-Did0lQxP.d.cts} +1 -1
- package/dist/{compose-BQYXdpar.d.ts → compose-6UOPEknN.d.ts} +1 -1
- package/dist/{compose-lt-Qz3lI.d.cts → compose-DhvoUrWa.d.cts} +1 -1
- package/dist/generated-api.d.cts +5 -5
- package/dist/generated-api.d.ts +5 -5
- package/dist/generated-runtime.d.cts +2 -2
- package/dist/generated-runtime.d.ts +2 -2
- package/dist/index.cjs +413 -147
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +295 -9
- package/dist/index.d.ts +295 -9
- package/dist/index.js +411 -148
- package/dist/index.js.map +1 -1
- package/dist/mcp-generated.d.cts +2 -2
- package/dist/mcp-generated.d.ts +2 -2
- package/dist/{runtime-CCm4CtU_.d.ts → runtime-CIQYCRL3.d.ts} +1 -1
- package/dist/{runtime-DTtWCxDs.d.cts → runtime-DmI1cOEq.d.cts} +1 -1
- package/dist/skills.d.cts +3 -3
- package/dist/skills.d.ts +3 -3
- package/dist/{types-BtQexFpT.d.cts → types-B57ZZfp3.d.cts} +15 -1
- package/dist/{types-BtQexFpT.d.ts → types-B57ZZfp3.d.ts} +15 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1037,6 +1037,27 @@ var ATTR_OLLAMA_AGENT_MAX_ITERATIONS = "ollama.agent.max_iterations";
|
|
|
1037
1037
|
var ATTR_OLLAMA_AGENT_ITERATION = "ollama.agent.iteration";
|
|
1038
1038
|
var GEN_AI_SYSTEM_OLLAMA = "ollama";
|
|
1039
1039
|
|
|
1040
|
+
// src/telemetry/metrics.ts
|
|
1041
|
+
function nsToMs(ns) {
|
|
1042
|
+
return ns ? Math.round(ns / 1e6 * 100) / 100 : 0;
|
|
1043
|
+
}
|
|
1044
|
+
function formatTelemetry(raw) {
|
|
1045
|
+
const evalDurationSec = raw.eval_duration ? raw.eval_duration / 1e9 : 0;
|
|
1046
|
+
const promptDurationSec = raw.prompt_eval_duration ? raw.prompt_eval_duration / 1e9 : 0;
|
|
1047
|
+
const promptEvaluated = raw.prompt_eval_count ?? 0;
|
|
1048
|
+
const promptCached = raw.prompt_eval_cached_count ?? 0;
|
|
1049
|
+
const promptTotal = promptCached + promptEvaluated;
|
|
1050
|
+
return {
|
|
1051
|
+
totalLatencyMs: nsToMs(raw.total_duration),
|
|
1052
|
+
modelLoadMs: nsToMs(raw.load_duration),
|
|
1053
|
+
promptEvalMs: nsToMs(raw.prompt_eval_duration),
|
|
1054
|
+
generationMs: nsToMs(raw.eval_duration),
|
|
1055
|
+
tokensPerSecond: evalDurationSec > 0 && raw.eval_count ? Math.round(raw.eval_count / evalDurationSec * 10) / 10 : 0,
|
|
1056
|
+
promptTokensPerSecond: promptDurationSec > 0 && promptEvaluated ? Math.round(promptEvaluated / promptDurationSec * 10) / 10 : 0,
|
|
1057
|
+
cacheHitRatio: promptTotal > 0 ? Math.round(promptCached / promptTotal * 1e3) / 1e3 : 0
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1040
1061
|
// src/transport/http.ts
|
|
1041
1062
|
function routeTemplate(path) {
|
|
1042
1063
|
return path.replace(/^\/api\/blobs\/.+$/, "/api/blobs/{digest}");
|
|
@@ -1574,6 +1595,272 @@ async function listAvailableModels(http) {
|
|
|
1574
1595
|
});
|
|
1575
1596
|
return [...res.models];
|
|
1576
1597
|
}
|
|
1598
|
+
|
|
1599
|
+
// src/context-safety.ts
|
|
1600
|
+
var DEFAULT_CONTEXT_WARNING_THRESHOLD = 0.9;
|
|
1601
|
+
var IMAGE_TOKEN_ESTIMATE = 300;
|
|
1602
|
+
var CHARS_PER_TOKEN = 4;
|
|
1603
|
+
function isCjkCodePoint(code) {
|
|
1604
|
+
return code >= 19968 && code <= 40959 || // CJK Unified Ideographs
|
|
1605
|
+
code >= 13312 && code <= 19903 || // CJK Extension A
|
|
1606
|
+
code >= 12352 && code <= 12543 || // Hiragana + Katakana
|
|
1607
|
+
code >= 44032 && code <= 55215 || // Hangul syllables
|
|
1608
|
+
code >= 63744 && code <= 64255 || // CJK Compatibility Ideographs
|
|
1609
|
+
code >= 65280 && code <= 65519;
|
|
1610
|
+
}
|
|
1611
|
+
function estimateTokens(text) {
|
|
1612
|
+
if (text.length === 0) return 0;
|
|
1613
|
+
let cjk = 0;
|
|
1614
|
+
let other = 0;
|
|
1615
|
+
for (const char of text) {
|
|
1616
|
+
if (isCjkCodePoint(char.codePointAt(0) ?? 0)) cjk += 1;
|
|
1617
|
+
else other += 1;
|
|
1618
|
+
}
|
|
1619
|
+
return Math.ceil(cjk + other / CHARS_PER_TOKEN);
|
|
1620
|
+
}
|
|
1621
|
+
var MESSAGE_OVERHEAD_TOKENS = 4;
|
|
1622
|
+
function estimateMessageTokens(message) {
|
|
1623
|
+
let total = MESSAGE_OVERHEAD_TOKENS + estimateTokens(message.content ?? "");
|
|
1624
|
+
if (message.thinking !== void 0) total += estimateTokens(message.thinking);
|
|
1625
|
+
if (message.tool_calls !== void 0) {
|
|
1626
|
+
for (const call of message.tool_calls) {
|
|
1627
|
+
total += estimateTokens(call.function.name);
|
|
1628
|
+
total += estimateTokens(JSON.stringify(call.function.arguments ?? {}));
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (message.images !== void 0) total += message.images.length * IMAGE_TOKEN_ESTIMATE;
|
|
1632
|
+
return total;
|
|
1633
|
+
}
|
|
1634
|
+
function estimateChatRequestTokens(req) {
|
|
1635
|
+
let total = 0;
|
|
1636
|
+
for (const message of req.messages) total += estimateMessageTokens(message);
|
|
1637
|
+
if (req.tools !== void 0) {
|
|
1638
|
+
for (const tool of req.tools) {
|
|
1639
|
+
total += estimateTokens(tool.function.description ?? "");
|
|
1640
|
+
total += estimateTokens(JSON.stringify(tool.function.parameters ?? {}));
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return total;
|
|
1644
|
+
}
|
|
1645
|
+
function estimateGenerateRequestTokens(req) {
|
|
1646
|
+
let total = estimateTokens(req.prompt ?? "");
|
|
1647
|
+
if (req.system !== void 0) total += estimateTokens(req.system);
|
|
1648
|
+
if (req.template !== void 0) total += estimateTokens(req.template);
|
|
1649
|
+
if (req.images !== void 0) total += req.images.length * IMAGE_TOKEN_ESTIMATE;
|
|
1650
|
+
return total;
|
|
1651
|
+
}
|
|
1652
|
+
function checkChatContext(req, options = {}) {
|
|
1653
|
+
return check(estimateChatRequestTokens(req), req.options?.num_ctx, options);
|
|
1654
|
+
}
|
|
1655
|
+
function checkGenerateContext(req, options = {}) {
|
|
1656
|
+
return check(estimateGenerateRequestTokens(req), req.options?.num_ctx, options);
|
|
1657
|
+
}
|
|
1658
|
+
function check(estimatedPromptTokens, requestNumCtx, options) {
|
|
1659
|
+
const threshold = options.threshold ?? DEFAULT_CONTEXT_WARNING_THRESHOLD;
|
|
1660
|
+
if (requestNumCtx !== void 0) {
|
|
1661
|
+
return {
|
|
1662
|
+
estimatedPromptTokens,
|
|
1663
|
+
effectiveContextLength: requestNumCtx,
|
|
1664
|
+
windowSource: "request",
|
|
1665
|
+
exceedsThreshold: estimatedPromptTokens > requestNumCtx * threshold
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
if (options.defaultContextLength !== void 0) {
|
|
1669
|
+
return {
|
|
1670
|
+
estimatedPromptTokens,
|
|
1671
|
+
effectiveContextLength: options.defaultContextLength,
|
|
1672
|
+
windowSource: "client-default",
|
|
1673
|
+
exceedsThreshold: estimatedPromptTokens > options.defaultContextLength * threshold
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
return {
|
|
1677
|
+
estimatedPromptTokens,
|
|
1678
|
+
effectiveContextLength: OLLAMA_FALLBACK_CONTEXT_LENGTH,
|
|
1679
|
+
windowSource: "server-fallback",
|
|
1680
|
+
exceedsThreshold: estimatedPromptTokens > OLLAMA_FALLBACK_CONTEXT_LENGTH
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
var OLLAMA_FALLBACK_CONTEXT_LENGTH = 2048;
|
|
1684
|
+
function contextWarningMessage(check2, kind) {
|
|
1685
|
+
const where = `OllamaClient.${kind}()`;
|
|
1686
|
+
const estimate = `~${check2.estimatedPromptTokens} tokens`;
|
|
1687
|
+
if (check2.windowSource === "request") {
|
|
1688
|
+
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\`.`;
|
|
1689
|
+
}
|
|
1690
|
+
if (check2.windowSource === "client-default") {
|
|
1691
|
+
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\`.`;
|
|
1692
|
+
}
|
|
1693
|
+
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\`.`;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/context-discovery.ts
|
|
1697
|
+
function extractParameterNumCtx(parameters) {
|
|
1698
|
+
if (!parameters) return void 0;
|
|
1699
|
+
const match = /^[ \t]*num_ctx[ \t]+(\d+)\s*$/m.exec(parameters);
|
|
1700
|
+
if (match === null) return void 0;
|
|
1701
|
+
const value = Number.parseInt(match[1], 10);
|
|
1702
|
+
return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1703
|
+
}
|
|
1704
|
+
function matchesModelReference(entry, model) {
|
|
1705
|
+
const exact = entry.name === model || entry.model === model;
|
|
1706
|
+
const taglessPrefix = !model.includes(":") && (entry.name !== void 0 && entry.name.startsWith(`${model}:`) || entry.model !== void 0 && entry.model.startsWith(`${model}:`));
|
|
1707
|
+
return exact || taglessPrefix;
|
|
1708
|
+
}
|
|
1709
|
+
function isModelResident(psModels, model) {
|
|
1710
|
+
return psModels.some((entry) => matchesModelReference(entry, model));
|
|
1711
|
+
}
|
|
1712
|
+
function findRunningModelContextLength(psModels, model) {
|
|
1713
|
+
for (const entry of psModels) {
|
|
1714
|
+
if (matchesModelReference(entry, model)) {
|
|
1715
|
+
return typeof entry.context_length === "number" && Number.isFinite(entry.context_length) && entry.context_length > 0 ? entry.context_length : void 0;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
return void 0;
|
|
1719
|
+
}
|
|
1720
|
+
function positiveInt(value) {
|
|
1721
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1722
|
+
}
|
|
1723
|
+
function resolveContextLength(signals) {
|
|
1724
|
+
const running = positiveInt(signals.running);
|
|
1725
|
+
const parameter = positiveInt(signals.parameter);
|
|
1726
|
+
const native = positiveInt(signals.native);
|
|
1727
|
+
if (running !== void 0) {
|
|
1728
|
+
return {
|
|
1729
|
+
contextLength: running,
|
|
1730
|
+
source: "running",
|
|
1731
|
+
...parameter !== void 0 ? { parameterContextLength: parameter } : {},
|
|
1732
|
+
...native !== void 0 ? { nativeContextLength: native } : {},
|
|
1733
|
+
runningContextLength: running
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
if (parameter !== void 0) {
|
|
1737
|
+
return {
|
|
1738
|
+
contextLength: parameter,
|
|
1739
|
+
source: "parameters",
|
|
1740
|
+
parameterContextLength: parameter,
|
|
1741
|
+
...native !== void 0 ? { nativeContextLength: native } : {}
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
if (native !== void 0) {
|
|
1745
|
+
return { contextLength: native, source: "model-info", nativeContextLength: native };
|
|
1746
|
+
}
|
|
1747
|
+
return { contextLength: OLLAMA_FALLBACK_CONTEXT_LENGTH, source: "fallback" };
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// src/providers/model-affinity-router.ts
|
|
1751
|
+
var DEFAULT_TTL_MS = 3e4;
|
|
1752
|
+
var DEFAULT_FAILURE_RETRY_MS = 5e3;
|
|
1753
|
+
var ModelAffinityRouter = class {
|
|
1754
|
+
fetchPs;
|
|
1755
|
+
ttlMs;
|
|
1756
|
+
failureRetryMs;
|
|
1757
|
+
now;
|
|
1758
|
+
cache = /* @__PURE__ */ new Map();
|
|
1759
|
+
failedAt = /* @__PURE__ */ new Map();
|
|
1760
|
+
inflight = /* @__PURE__ */ new Map();
|
|
1761
|
+
constructor(fetchPs, options = {}) {
|
|
1762
|
+
this.fetchPs = fetchPs;
|
|
1763
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
1764
|
+
this.failureRetryMs = options.failureRetryMs ?? DEFAULT_FAILURE_RETRY_MS;
|
|
1765
|
+
this.now = options.now ?? Date.now;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Synchronously reorders `candidates` so that, within each priority tier
|
|
1769
|
+
* (run of consecutive equal `priority`), endpoints whose fresh `/api/ps`
|
|
1770
|
+
* snapshot shows `model` resident come first (relative order preserved within
|
|
1771
|
+
* each group). Never moves an endpoint across tiers. Returns the input unchanged — and kicks a background refresh —
|
|
1772
|
+
* when any candidate's snapshot is missing or stale: a wrong reorder is worse
|
|
1773
|
+
* than no reorder, and the first request after a cache miss simply isn't
|
|
1774
|
+
* reordered. No-op without a `model` or with fewer than two candidates.
|
|
1775
|
+
*/
|
|
1776
|
+
reorder(candidates, model) {
|
|
1777
|
+
if (model === void 0 || candidates.length < 2) return candidates;
|
|
1778
|
+
const currentTime = this.now();
|
|
1779
|
+
let allFresh = true;
|
|
1780
|
+
for (const endpoint of candidates) {
|
|
1781
|
+
const entry = this.cache.get(endpoint.name);
|
|
1782
|
+
if (entry === void 0 || currentTime - entry.fetchedAt > this.ttlMs) {
|
|
1783
|
+
allFresh = false;
|
|
1784
|
+
this.kickRefresh(endpoint);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
if (!allFresh) return candidates;
|
|
1788
|
+
const isResident = (ep) => {
|
|
1789
|
+
const entry = this.cache.get(ep.name);
|
|
1790
|
+
return entry !== void 0 && isModelResident(entry.models, model);
|
|
1791
|
+
};
|
|
1792
|
+
const reordered = [];
|
|
1793
|
+
let changed = false;
|
|
1794
|
+
for (let start = 0; start < candidates.length; ) {
|
|
1795
|
+
const tierPriority = candidates[start].priority ?? 0;
|
|
1796
|
+
let end = start + 1;
|
|
1797
|
+
while (end < candidates.length && (candidates[end].priority ?? 0) === tierPriority) end++;
|
|
1798
|
+
const tier = candidates.slice(start, end);
|
|
1799
|
+
const resident = tier.filter(isResident);
|
|
1800
|
+
if (resident.length === 0 || resident.length === tier.length) {
|
|
1801
|
+
reordered.push(...tier);
|
|
1802
|
+
} else {
|
|
1803
|
+
const tierOrder = [...resident, ...tier.filter((ep) => !isResident(ep))];
|
|
1804
|
+
changed ||= tierOrder.some((ep, i) => ep !== tier[i]);
|
|
1805
|
+
reordered.push(...tierOrder);
|
|
1806
|
+
}
|
|
1807
|
+
start = end;
|
|
1808
|
+
}
|
|
1809
|
+
return changed ? reordered : candidates;
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Forces a refresh of the affinity cache for the given endpoints (deduped with
|
|
1813
|
+
* any in-flight refresh; bypasses the failure backoff, because this is an
|
|
1814
|
+
* explicit caller request — `client.warmModelAffinity()`). Resolves once every
|
|
1815
|
+
* probe has settled — successfully or not; individual failures never reject.
|
|
1816
|
+
*/
|
|
1817
|
+
async warm(endpoints) {
|
|
1818
|
+
await Promise.all(endpoints.map((endpoint) => this.refresh(endpoint)));
|
|
1819
|
+
}
|
|
1820
|
+
/** Introspection snapshots for the given endpoints (defaults to cached ones). */
|
|
1821
|
+
status(endpoints) {
|
|
1822
|
+
const names = endpoints !== void 0 ? endpoints.map((ep) => ep.name) : [...this.cache.keys(), ...this.failedAt.keys()];
|
|
1823
|
+
const byName = new Map((endpoints ?? []).map((ep) => [ep.name, ep]));
|
|
1824
|
+
const currentTime = this.now();
|
|
1825
|
+
return names.filter((name, i, arr) => arr.indexOf(name) === i).map((name) => {
|
|
1826
|
+
const entry = this.cache.get(name);
|
|
1827
|
+
return {
|
|
1828
|
+
endpointName: name,
|
|
1829
|
+
baseUrl: byName.get(name)?.baseUrl ?? "",
|
|
1830
|
+
loadedModels: entry?.models.map((m) => m.name ?? m.model ?? "").filter((n) => n !== "") ?? [],
|
|
1831
|
+
fetchedAt: entry?.fetchedAt,
|
|
1832
|
+
fresh: entry !== void 0 && currentTime - entry.fetchedAt <= this.ttlMs
|
|
1833
|
+
};
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Starts (or joins) a background refresh for `endpoint`, unless one is already
|
|
1838
|
+
* in flight or the failure backoff suppresses it. Fire-and-forget: the promise
|
|
1839
|
+
* is stored only for dedupe/warm-joins; callers never await it on the request
|
|
1840
|
+
* path.
|
|
1841
|
+
*/
|
|
1842
|
+
kickRefresh(endpoint) {
|
|
1843
|
+
if (this.inflight.has(endpoint.name)) return;
|
|
1844
|
+
const lastFailure = this.failedAt.get(endpoint.name);
|
|
1845
|
+
if (lastFailure !== void 0 && this.now() - lastFailure < this.failureRetryMs) return;
|
|
1846
|
+
void this.refresh(endpoint);
|
|
1847
|
+
}
|
|
1848
|
+
/** Refreshes one endpoint's snapshot; never rejects (failures record a backoff). */
|
|
1849
|
+
refresh(endpoint) {
|
|
1850
|
+
const existing = this.inflight.get(endpoint.name);
|
|
1851
|
+
if (existing !== void 0) return existing;
|
|
1852
|
+
const attempt = this.fetchPs(endpoint).then((models) => {
|
|
1853
|
+
this.cache.set(endpoint.name, { models, fetchedAt: this.now() });
|
|
1854
|
+
this.failedAt.delete(endpoint.name);
|
|
1855
|
+
}).catch(() => {
|
|
1856
|
+
this.failedAt.set(endpoint.name, this.now());
|
|
1857
|
+
}).finally(() => {
|
|
1858
|
+
this.inflight.delete(endpoint.name);
|
|
1859
|
+
});
|
|
1860
|
+
this.inflight.set(endpoint.name, attempt);
|
|
1861
|
+
return attempt;
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1577
1864
|
function extractJsonSubstring(text) {
|
|
1578
1865
|
const trimmed = text.trim();
|
|
1579
1866
|
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
@@ -2186,151 +2473,6 @@ function createTimeoutSignal(timeoutMs, userSignal) {
|
|
|
2186
2473
|
return { signal: controller.signal, cancel };
|
|
2187
2474
|
}
|
|
2188
2475
|
|
|
2189
|
-
// src/context-safety.ts
|
|
2190
|
-
var DEFAULT_CONTEXT_WARNING_THRESHOLD = 0.9;
|
|
2191
|
-
var IMAGE_TOKEN_ESTIMATE = 300;
|
|
2192
|
-
var CHARS_PER_TOKEN = 4;
|
|
2193
|
-
function isCjkCodePoint(code) {
|
|
2194
|
-
return code >= 19968 && code <= 40959 || // CJK Unified Ideographs
|
|
2195
|
-
code >= 13312 && code <= 19903 || // CJK Extension A
|
|
2196
|
-
code >= 12352 && code <= 12543 || // Hiragana + Katakana
|
|
2197
|
-
code >= 44032 && code <= 55215 || // Hangul syllables
|
|
2198
|
-
code >= 63744 && code <= 64255 || // CJK Compatibility Ideographs
|
|
2199
|
-
code >= 65280 && code <= 65519;
|
|
2200
|
-
}
|
|
2201
|
-
function estimateTokens(text) {
|
|
2202
|
-
if (text.length === 0) return 0;
|
|
2203
|
-
let cjk = 0;
|
|
2204
|
-
let other = 0;
|
|
2205
|
-
for (const char of text) {
|
|
2206
|
-
if (isCjkCodePoint(char.codePointAt(0) ?? 0)) cjk += 1;
|
|
2207
|
-
else other += 1;
|
|
2208
|
-
}
|
|
2209
|
-
return Math.ceil(cjk + other / CHARS_PER_TOKEN);
|
|
2210
|
-
}
|
|
2211
|
-
var MESSAGE_OVERHEAD_TOKENS = 4;
|
|
2212
|
-
function estimateMessageTokens(message) {
|
|
2213
|
-
let total = MESSAGE_OVERHEAD_TOKENS + estimateTokens(message.content ?? "");
|
|
2214
|
-
if (message.thinking !== void 0) total += estimateTokens(message.thinking);
|
|
2215
|
-
if (message.tool_calls !== void 0) {
|
|
2216
|
-
for (const call of message.tool_calls) {
|
|
2217
|
-
total += estimateTokens(call.function.name);
|
|
2218
|
-
total += estimateTokens(JSON.stringify(call.function.arguments ?? {}));
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
if (message.images !== void 0) total += message.images.length * IMAGE_TOKEN_ESTIMATE;
|
|
2222
|
-
return total;
|
|
2223
|
-
}
|
|
2224
|
-
function estimateChatRequestTokens(req) {
|
|
2225
|
-
let total = 0;
|
|
2226
|
-
for (const message of req.messages) total += estimateMessageTokens(message);
|
|
2227
|
-
if (req.tools !== void 0) {
|
|
2228
|
-
for (const tool of req.tools) {
|
|
2229
|
-
total += estimateTokens(tool.function.description ?? "");
|
|
2230
|
-
total += estimateTokens(JSON.stringify(tool.function.parameters ?? {}));
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
return total;
|
|
2234
|
-
}
|
|
2235
|
-
function estimateGenerateRequestTokens(req) {
|
|
2236
|
-
let total = estimateTokens(req.prompt ?? "");
|
|
2237
|
-
if (req.system !== void 0) total += estimateTokens(req.system);
|
|
2238
|
-
if (req.template !== void 0) total += estimateTokens(req.template);
|
|
2239
|
-
if (req.images !== void 0) total += req.images.length * IMAGE_TOKEN_ESTIMATE;
|
|
2240
|
-
return total;
|
|
2241
|
-
}
|
|
2242
|
-
function checkChatContext(req, options = {}) {
|
|
2243
|
-
return check(estimateChatRequestTokens(req), req.options?.num_ctx, options);
|
|
2244
|
-
}
|
|
2245
|
-
function checkGenerateContext(req, options = {}) {
|
|
2246
|
-
return check(estimateGenerateRequestTokens(req), req.options?.num_ctx, options);
|
|
2247
|
-
}
|
|
2248
|
-
function check(estimatedPromptTokens, requestNumCtx, options) {
|
|
2249
|
-
const threshold = options.threshold ?? DEFAULT_CONTEXT_WARNING_THRESHOLD;
|
|
2250
|
-
if (requestNumCtx !== void 0) {
|
|
2251
|
-
return {
|
|
2252
|
-
estimatedPromptTokens,
|
|
2253
|
-
effectiveContextLength: requestNumCtx,
|
|
2254
|
-
windowSource: "request",
|
|
2255
|
-
exceedsThreshold: estimatedPromptTokens > requestNumCtx * threshold
|
|
2256
|
-
};
|
|
2257
|
-
}
|
|
2258
|
-
if (options.defaultContextLength !== void 0) {
|
|
2259
|
-
return {
|
|
2260
|
-
estimatedPromptTokens,
|
|
2261
|
-
effectiveContextLength: options.defaultContextLength,
|
|
2262
|
-
windowSource: "client-default",
|
|
2263
|
-
exceedsThreshold: estimatedPromptTokens > options.defaultContextLength * threshold
|
|
2264
|
-
};
|
|
2265
|
-
}
|
|
2266
|
-
return {
|
|
2267
|
-
estimatedPromptTokens,
|
|
2268
|
-
effectiveContextLength: OLLAMA_FALLBACK_CONTEXT_LENGTH,
|
|
2269
|
-
windowSource: "server-fallback",
|
|
2270
|
-
exceedsThreshold: estimatedPromptTokens > OLLAMA_FALLBACK_CONTEXT_LENGTH
|
|
2271
|
-
};
|
|
2272
|
-
}
|
|
2273
|
-
var OLLAMA_FALLBACK_CONTEXT_LENGTH = 2048;
|
|
2274
|
-
function contextWarningMessage(check2, kind) {
|
|
2275
|
-
const where = `OllamaClient.${kind}()`;
|
|
2276
|
-
const estimate = `~${check2.estimatedPromptTokens} tokens`;
|
|
2277
|
-
if (check2.windowSource === "request") {
|
|
2278
|
-
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\`.`;
|
|
2279
|
-
}
|
|
2280
|
-
if (check2.windowSource === "client-default") {
|
|
2281
|
-
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\`.`;
|
|
2282
|
-
}
|
|
2283
|
-
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\`.`;
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
// src/context-discovery.ts
|
|
2287
|
-
function extractParameterNumCtx(parameters) {
|
|
2288
|
-
if (!parameters) return void 0;
|
|
2289
|
-
const match = /^[ \t]*num_ctx[ \t]+(\d+)\s*$/m.exec(parameters);
|
|
2290
|
-
if (match === null) return void 0;
|
|
2291
|
-
const value = Number.parseInt(match[1], 10);
|
|
2292
|
-
return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
2293
|
-
}
|
|
2294
|
-
function findRunningModelContextLength(psModels, model) {
|
|
2295
|
-
for (const entry of psModels) {
|
|
2296
|
-
const exact = entry.name === model || entry.model === model;
|
|
2297
|
-
const taglessPrefix = !model.includes(":") && (entry.name.startsWith(`${model}:`) || entry.model.startsWith(`${model}:`));
|
|
2298
|
-
if (exact || taglessPrefix) {
|
|
2299
|
-
return typeof entry.context_length === "number" && Number.isFinite(entry.context_length) && entry.context_length > 0 ? entry.context_length : void 0;
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
return void 0;
|
|
2303
|
-
}
|
|
2304
|
-
function positiveInt(value) {
|
|
2305
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
2306
|
-
}
|
|
2307
|
-
function resolveContextLength(signals) {
|
|
2308
|
-
const running = positiveInt(signals.running);
|
|
2309
|
-
const parameter = positiveInt(signals.parameter);
|
|
2310
|
-
const native = positiveInt(signals.native);
|
|
2311
|
-
if (running !== void 0) {
|
|
2312
|
-
return {
|
|
2313
|
-
contextLength: running,
|
|
2314
|
-
source: "running",
|
|
2315
|
-
...parameter !== void 0 ? { parameterContextLength: parameter } : {},
|
|
2316
|
-
...native !== void 0 ? { nativeContextLength: native } : {},
|
|
2317
|
-
runningContextLength: running
|
|
2318
|
-
};
|
|
2319
|
-
}
|
|
2320
|
-
if (parameter !== void 0) {
|
|
2321
|
-
return {
|
|
2322
|
-
contextLength: parameter,
|
|
2323
|
-
source: "parameters",
|
|
2324
|
-
parameterContextLength: parameter,
|
|
2325
|
-
...native !== void 0 ? { nativeContextLength: native } : {}
|
|
2326
|
-
};
|
|
2327
|
-
}
|
|
2328
|
-
if (native !== void 0) {
|
|
2329
|
-
return { contextLength: native, source: "model-info", nativeContextLength: native };
|
|
2330
|
-
}
|
|
2331
|
-
return { contextLength: OLLAMA_FALLBACK_CONTEXT_LENGTH, source: "fallback" };
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
2476
|
// src/keep-alive.ts
|
|
2335
2477
|
var KEEP_ALIVE_UNLOAD = 0;
|
|
2336
2478
|
var KEEP_ALIVE_INDEFINITE = -1;
|
|
@@ -5317,7 +5459,7 @@ function createLogicalRequestId() {
|
|
|
5317
5459
|
logicalRequestSequence += 1;
|
|
5318
5460
|
return `ollama-request-${logicalRequestSequence}`;
|
|
5319
5461
|
}
|
|
5320
|
-
var OllamaClient = class {
|
|
5462
|
+
var OllamaClient = class _OllamaClient {
|
|
5321
5463
|
registry;
|
|
5322
5464
|
models;
|
|
5323
5465
|
retryConfig;
|
|
@@ -5349,6 +5491,16 @@ var OllamaClient = class {
|
|
|
5349
5491
|
* (streams, via `executeWithFailover`'s `holdUntil` release path).
|
|
5350
5492
|
*/
|
|
5351
5493
|
activeRequests = /* @__PURE__ */ new Set();
|
|
5494
|
+
/**
|
|
5495
|
+
* Dynamic multi-host model-affinity routing, active only when
|
|
5496
|
+
* `config.endpointHealth.modelAffinity` is set — see
|
|
5497
|
+
* `src/providers/model-affinity-router.ts` and ADR 0028. `undefined` means
|
|
5498
|
+
* the request path skips reordering entirely (zero overhead, and no `/api/ps`
|
|
5499
|
+
* probes are ever issued).
|
|
5500
|
+
*/
|
|
5501
|
+
modelAffinity;
|
|
5502
|
+
/** Default timeout for one-off probe requests (`ping`, affinity `/api/ps` refreshes). */
|
|
5503
|
+
static PROBE_TIMEOUT_MS = 5e3;
|
|
5352
5504
|
constructor(config = {}) {
|
|
5353
5505
|
this.cloudApiKey = resolveApiKey(config.apiKey);
|
|
5354
5506
|
const defaultBaseUrl = resolveBaseUrl(config.baseUrl);
|
|
@@ -5376,6 +5528,11 @@ var OllamaClient = class {
|
|
|
5376
5528
|
this.defaultContextLength = config.defaultContextLength;
|
|
5377
5529
|
this.contextWarningThreshold = config.contextWarningThreshold;
|
|
5378
5530
|
this.onContextOverflow = config.onContextOverflow ?? "warn";
|
|
5531
|
+
const modelAffinityOptions = config.endpointHealth?.modelAffinity;
|
|
5532
|
+
this.modelAffinity = modelAffinityOptions !== void 0 ? new ModelAffinityRouter(
|
|
5533
|
+
(endpoint) => this.fetchEndpointRunningModels(endpoint),
|
|
5534
|
+
modelAffinityOptions
|
|
5535
|
+
) : void 0;
|
|
5379
5536
|
this.models = new ModelsClient((op, opts) => this.executeWithFailover(op, opts));
|
|
5380
5537
|
}
|
|
5381
5538
|
get modelsClient() {
|
|
@@ -5586,7 +5743,8 @@ var OllamaClient = class {
|
|
|
5586
5743
|
);
|
|
5587
5744
|
continue;
|
|
5588
5745
|
}
|
|
5589
|
-
|
|
5746
|
+
const ordered = this.modelAffinity !== void 0 ? this.modelAffinity.reorder(runnable, options?.model) : runnable;
|
|
5747
|
+
for (const [attemptIndex, endpoint] of ordered.entries()) {
|
|
5590
5748
|
this.logger.debug(`Executing on endpoint "${endpoint.name}" (${endpoint.baseUrl})`);
|
|
5591
5749
|
const http = new HttpClient({
|
|
5592
5750
|
baseUrl: endpoint.baseUrl,
|
|
@@ -6108,9 +6266,114 @@ var OllamaClient = class {
|
|
|
6108
6266
|
)
|
|
6109
6267
|
);
|
|
6110
6268
|
}
|
|
6269
|
+
/**
|
|
6270
|
+
* Ultra-lightweight liveness probe against the client's current best
|
|
6271
|
+
* endpoint: `HEAD /` — the root ping Ollama's documentation defines for
|
|
6272
|
+
* load-balancer health checks (HTTP 200; the daemon answers `GET /` with the
|
|
6273
|
+
* plain-text body "Ollama is running", which a HEAD request doesn't transfer).
|
|
6274
|
+
*
|
|
6275
|
+
* Deliberately a *single-endpoint, single-shot* probe — no failover to other
|
|
6276
|
+
* candidates, no retry — so the answer reflects one concrete host, not "some
|
|
6277
|
+
* endpoint eventually answered". To audit every configured endpoint, use
|
|
6278
|
+
* {@link healthCheck} (which probes all of them, and also reports the server
|
|
6279
|
+
* version at the cost of the heavier `GET /api/version` round trip).
|
|
6280
|
+
*
|
|
6281
|
+
* Resolves — never throws — with `{ healthy: false, error }` for connection
|
|
6282
|
+
* failures and timeouts; only caller-initiated cancellation (`signal`/
|
|
6283
|
+
* `destroy()`) rejects, mirroring how any other aborted request behaves.
|
|
6284
|
+
*/
|
|
6285
|
+
async ping(options) {
|
|
6286
|
+
const endpoint = this.registry.candidates()[0];
|
|
6287
|
+
if (endpoint === void 0) {
|
|
6288
|
+
return { healthy: false, latencyMs: 0, baseUrl: "", error: "No configured endpoints" };
|
|
6289
|
+
}
|
|
6290
|
+
const http = new HttpClient({
|
|
6291
|
+
baseUrl: endpoint.baseUrl,
|
|
6292
|
+
...endpoint.apiKey !== void 0 ? { apiKey: endpoint.apiKey } : {},
|
|
6293
|
+
...endpoint.headers !== void 0 ? { headers: endpoint.headers } : {},
|
|
6294
|
+
fetch: this.fetchImpl,
|
|
6295
|
+
middleware: this.middleware,
|
|
6296
|
+
onLifecycleEvent: this.onLifecycleEvent
|
|
6297
|
+
});
|
|
6298
|
+
const timeout = createTimeoutSignal(
|
|
6299
|
+
options?.timeoutMs ?? _OllamaClient.PROBE_TIMEOUT_MS,
|
|
6300
|
+
options?.signal
|
|
6301
|
+
);
|
|
6302
|
+
const startTime = Date.now();
|
|
6303
|
+
try {
|
|
6304
|
+
await http.request({ path: "/", method: "HEAD", signal: timeout.signal });
|
|
6305
|
+
return { healthy: true, latencyMs: Date.now() - startTime, baseUrl: endpoint.baseUrl };
|
|
6306
|
+
} catch (err) {
|
|
6307
|
+
if (options?.signal?.aborted) {
|
|
6308
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
6309
|
+
}
|
|
6310
|
+
return {
|
|
6311
|
+
healthy: false,
|
|
6312
|
+
latencyMs: Date.now() - startTime,
|
|
6313
|
+
baseUrl: endpoint.baseUrl,
|
|
6314
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6315
|
+
};
|
|
6316
|
+
} finally {
|
|
6317
|
+
timeout.cancel();
|
|
6318
|
+
}
|
|
6319
|
+
}
|
|
6320
|
+
/**
|
|
6321
|
+
* Affinity-cache snapshots for every configured endpoint — which models each
|
|
6322
|
+
* host held in VRAM at the last successful `/api/ps` probe, when that was, and
|
|
6323
|
+
* whether it's still fresh. Empty when `endpointHealth.modelAffinity` isn't
|
|
6324
|
+
* configured (no probes are ever issued, so there is nothing to report).
|
|
6325
|
+
*/
|
|
6326
|
+
modelAffinityStatus() {
|
|
6327
|
+
return this.modelAffinity?.status(this.registry.list()) ?? [];
|
|
6328
|
+
}
|
|
6329
|
+
/**
|
|
6330
|
+
* Explicitly refreshes the model-affinity cache for every configured endpoint
|
|
6331
|
+
* (deduped with any in-flight background refresh). No-op — resolves
|
|
6332
|
+
* immediately — when `endpointHealth.modelAffinity` isn't configured. Use it
|
|
6333
|
+
* to warm the cache at startup so the very first request already routes by
|
|
6334
|
+
* residency instead of waiting for the self-warming first-round refresh.
|
|
6335
|
+
*/
|
|
6336
|
+
async warmModelAffinity() {
|
|
6337
|
+
await this.modelAffinity?.warm(this.registry.list());
|
|
6338
|
+
}
|
|
6111
6339
|
endpointStatus() {
|
|
6112
6340
|
return this.registry.status();
|
|
6113
6341
|
}
|
|
6342
|
+
/**
|
|
6343
|
+
* Fetches one endpoint's `GET /api/ps` running-model list for the affinity
|
|
6344
|
+
* router. Bypasses `executeWithFailover` deliberately: probes must not
|
|
6345
|
+
* reenter the routing machinery they inform, must not touch the registry's
|
|
6346
|
+
* circuit-breaker/failure bookkeeping (a dead host's probe failing must not
|
|
6347
|
+
* cool it down harder than its real traffic already does), and must not fail
|
|
6348
|
+
* over to a different host (the answer is per-host by definition). Failures
|
|
6349
|
+
* propagate to the router, which records a short backoff and retries later.
|
|
6350
|
+
*/
|
|
6351
|
+
async fetchEndpointRunningModels(endpoint) {
|
|
6352
|
+
var _stack = [];
|
|
6353
|
+
try {
|
|
6354
|
+
const http = new HttpClient({
|
|
6355
|
+
baseUrl: endpoint.baseUrl,
|
|
6356
|
+
...endpoint.apiKey !== void 0 ? { apiKey: endpoint.apiKey } : {},
|
|
6357
|
+
...endpoint.headers !== void 0 ? { headers: endpoint.headers } : {},
|
|
6358
|
+
fetch: this.fetchImpl,
|
|
6359
|
+
middleware: this.middleware,
|
|
6360
|
+
onLifecycleEvent: this.onLifecycleEvent
|
|
6361
|
+
});
|
|
6362
|
+
const controller = new AbortController();
|
|
6363
|
+
const timer = setTimeout(() => controller.abort(), _OllamaClient.PROBE_TIMEOUT_MS);
|
|
6364
|
+
const _timer = __using(_stack, disposableTimer(timer));
|
|
6365
|
+
const res = await http.request({
|
|
6366
|
+
path: "/api/ps",
|
|
6367
|
+
method: "GET",
|
|
6368
|
+
signal: controller.signal
|
|
6369
|
+
});
|
|
6370
|
+
return res.models ?? [];
|
|
6371
|
+
} catch (_) {
|
|
6372
|
+
var _error = _, _hasError = true;
|
|
6373
|
+
} finally {
|
|
6374
|
+
__callDispose(_stack, _error, _hasError);
|
|
6375
|
+
}
|
|
6376
|
+
}
|
|
6114
6377
|
/**
|
|
6115
6378
|
* Immediately aborts every in-flight request and active stream managed by
|
|
6116
6379
|
* this client — the clean teardown path for short-lived processes, CLI
|
|
@@ -7512,6 +7775,7 @@ exports.MAX_SYSTEM_ONE_IMAGES_BYTES = MAX_SYSTEM_ONE_IMAGES_BYTES;
|
|
|
7512
7775
|
exports.MAX_SYSTEM_ONE_REQUEST_BYTES = MAX_SYSTEM_ONE_REQUEST_BYTES;
|
|
7513
7776
|
exports.MESSAGE_OVERHEAD_TOKENS = MESSAGE_OVERHEAD_TOKENS;
|
|
7514
7777
|
exports.McpBridge = McpBridge;
|
|
7778
|
+
exports.ModelAffinityRouter = ModelAffinityRouter;
|
|
7515
7779
|
exports.ModelAffinityScheduler = ModelAffinityScheduler;
|
|
7516
7780
|
exports.ModelsClient = ModelsClient;
|
|
7517
7781
|
exports.NANOS_PER_MS = NANOS_PER_MS;
|
|
@@ -7590,10 +7854,12 @@ exports.extractUsage = extractUsage;
|
|
|
7590
7854
|
exports.fetchWithBackoff = fetchWithBackoff;
|
|
7591
7855
|
exports.findOversizedEmbedInputs = findOversizedEmbedInputs;
|
|
7592
7856
|
exports.findRunningModelContextLength = findRunningModelContextLength;
|
|
7857
|
+
exports.formatTelemetry = formatTelemetry;
|
|
7593
7858
|
exports.imageStringNeedsResolution = imageStringNeedsResolution;
|
|
7594
7859
|
exports.inferRuntimeMode = inferRuntimeMode;
|
|
7595
7860
|
exports.isKeepAliveSugar = isKeepAliveSugar;
|
|
7596
7861
|
exports.isKnownDoneReason = isKnownDoneReason;
|
|
7862
|
+
exports.isModelResident = isModelResident;
|
|
7597
7863
|
exports.isSameToolCall = isSameToolCall;
|
|
7598
7864
|
exports.listAllMcpTools = listAllMcpTools;
|
|
7599
7865
|
exports.listAvailableModels = listAvailableModels;
|