@nemesis-oss/ollama-sdk 1.12.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 +9 -0
- package/README.md +16 -5
- package/dist/index.cjs +23 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +23 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.12.1] — Fix: model-affinity routing respects priority tiers
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`endpointHealth.modelAffinity` no longer overrides endpoint `priority`.** In 1.12.0, `ModelAffinityRouter.reorder()` moved every host holding the requested model to the front of the _whole_ candidate list, so a lower-priority host with the model in VRAM was tried before a higher-priority host without it. That contradicted the README and ADR 0028, which state that priority tiers keep working. Affinity now reorders only within each priority tier (runs of consecutive equal `priority`, unset = `0`, as in the registry): a higher-priority endpoint is always tried first, and residency only breaks ties inside a tier. The order between tiers, including the registry's fail-open recovery order, is unchanged.
|
|
8
|
+
- **Behavior change for configurations that mix `priority` values with `modelAffinity`:** requests that 1.12.0 sent to a resident lower-priority host now go to the higher-priority host first. Pools where every endpoint has the same priority (the documented fleet-of-workstations setup) are unaffected.
|
|
9
|
+
- 6 new tests: tier isolation, independent per-tier reordering, unset priority as `0`, consecutive-run grouping, an unchanged input returned as-is, and an end-to-end `OllamaClient` routing check.
|
|
10
|
+
- `package-lock.json` root version corrected (it still read `1.8.0`).
|
|
11
|
+
|
|
3
12
|
## [1.12.0] — Daemon liveness & telemetry standardization: formatTelemetry, dynamic host model-affinity routing, root ping
|
|
4
13
|
|
|
5
14
|
The October 10 documentation digest ("Daemon Liveness & Telemetry Standardization") reviewed the official operational surfaces — the root liveness probe (`HEAD /` → 200, body "Ollama is running"), `GET /api/version`, `GET /api/ps` runner introspection, and the nanosecond duration counters on every generation response. Verified against the codebase first (see [ADR 0028](./docs/adr/0028-telemetry-normalization-host-affinity-root-ping.md)): two of the digest's gap assessments were stale — multi-host pooling with least-connections routing, capacity caps, and 503/`OLLAMA_MAX_QUEUE` saturation failover already exist (`endpoints` + `endpointHealth` + `DEFAULT_FAILOVER_CODES`), so the proposed `LoadBalancedOllamaClient` was **not** added (it would fork the existing routing engine); the real gaps — telemetry math, dynamic multi-host affinity, and the root ping — are implemented, all additive, no breaking changes. 865 pre-existing tests stay green, 33 new tests added.
|
package/README.md
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
> Production-grade TypeScript SDK for Ollama. Built with native fetch, high availability failover, multi-turn tool calling, structured outputs with Zod, reasoning stream tokens, OpenAI & Anthropic compatibility bridges, MCP integration, and Web Stream adapters.
|
|
8
8
|
|
|
9
|
+
### How this compares
|
|
10
|
+
|
|
11
|
+
| Need | Typical choice |
|
|
12
|
+
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
13
|
+
| Default Ollama integration in JS/TS | Official [`ollama`](https://www.npmjs.com/package/ollama) client (`ollama/ollama-js`) |
|
|
14
|
+
| Vercel AI SDK (`ai` package) apps | [`ai-sdk-ollama`](https://www.npmjs.com/package/ai-sdk-ollama) |
|
|
15
|
+
| Agent runtime, MCP bridge, HA failover, context safety, structured outputs | **This package** — powers [Nexum](https://github.com/shubhamtaywade82/nexum) |
|
|
16
|
+
|
|
17
|
+
Nexum’s model harness (2026-10-09) exercised this SDK via `gemma4:cloud` and `minicpm5:2b` with 100% pass on `tool-calling`, `output-format`, and `agentic-looping` categories. See [nexum/docs/guide/benchmarks.md](../nexum/docs/guide/benchmarks.md) and [validation.md](../nexum/docs/guide/validation.md).
|
|
18
|
+
|
|
9
19
|
---
|
|
10
20
|
|
|
11
21
|
## Key Features
|
|
@@ -1174,7 +1184,7 @@ initial HTTP round trip:
|
|
|
1174
1184
|
|
|
1175
1185
|
#### Model-affinity routing across hosts (prefer the resident host)
|
|
1176
1186
|
|
|
1177
|
-
With several
|
|
1187
|
+
With several _interchangeable_ endpoints serving the same models — the fleet-of-workstations
|
|
1178
1188
|
case, not the per-credential Cloud case — sending a request to a host that doesn't currently
|
|
1179
1189
|
hold the model forces that host to evict something and cold-load it: the multi-host version
|
|
1180
1190
|
of the thrashing `ModelAffinityScheduler` prevents inside one client. Opt in with
|
|
@@ -1217,7 +1227,8 @@ The design never trades latency for affinity (see
|
|
|
1217
1227
|
from being re-probed on every request.
|
|
1218
1228
|
- **No probes at all unless configured.** Without `modelAffinity`, the router isn't even
|
|
1219
1229
|
constructed — zero overhead, and no `/api/ps` traffic.
|
|
1220
|
-
- **Composes with everything above.** Affinity reorders
|
|
1230
|
+
- **Composes with everything above.** Affinity reorders _within each priority tier_ of
|
|
1231
|
+
the runnable set (a higher-priority host is always tried first, resident or not):
|
|
1221
1232
|
priority tiers, `least-connections` ordering, capacity caps, and `models`-scoped
|
|
1222
1233
|
credentials keep working exactly as before, and single-endpoint-pinned operations
|
|
1223
1234
|
(model/blob management, `capabilities()`) are never moved to a different host.
|
|
@@ -1225,7 +1236,7 @@ The design never trades latency for affinity (see
|
|
|
1225
1236
|
`llama3:latest` but never `llama3.1`), shared with context-window discovery so the
|
|
1226
1237
|
two features can't disagree about what "loaded" means.
|
|
1227
1238
|
|
|
1228
|
-
For the
|
|
1239
|
+
For the _single-host_ counterpart — per-model serial queues that deepen the already-hot
|
|
1229
1240
|
model before loading the next one — see
|
|
1230
1241
|
[Model-Affinity Scheduling](#model-affinity-scheduling-multi-model-anti-thrashing); the
|
|
1231
1242
|
two compose: a scheduler per host, affinity routing between hosts.
|
|
@@ -1305,7 +1316,7 @@ Two behaviors worth knowing:
|
|
|
1305
1316
|
servers without `prompt_eval_cached_count`, embeddings) format cleanly instead of
|
|
1306
1317
|
producing `NaN`/`Infinity`, and zero durations never divide into rates.
|
|
1307
1318
|
- **`cacheHitRatio` uses `cached / (cached + evaluated)`**, not
|
|
1308
|
-
`cached / prompt_eval_count`: Ollama reports `prompt_eval_count: 0` on a
|
|
1319
|
+
`cached / prompt_eval_count`: Ollama reports `prompt_eval_count: 0` on a _full_ cache
|
|
1309
1320
|
hit, so dividing by it alone would read a perfect cache as "no caching at all". This
|
|
1310
1321
|
is the same semantics as [`ConversationSession`'s
|
|
1311
1322
|
`cacheStats.hitRate`](#kv-cache-aware-conversation-sessions), so the two surfaces can
|
|
@@ -1315,7 +1326,7 @@ Two behaviors worth knowing:
|
|
|
1315
1326
|
straight into log lines, dashboards, and streaming pipelines alike. For span-based
|
|
1316
1327
|
instrumentation of requests themselves, see
|
|
1317
1328
|
[Observability with OpenTelemetry](#observability-with-opentelemetry) below; the two
|
|
1318
|
-
compose (spans for
|
|
1329
|
+
compose (spans for _where/when_, `formatTelemetry` for _how fast_).
|
|
1319
1330
|
|
|
1320
1331
|
### Observability with OpenTelemetry
|
|
1321
1332
|
|
package/dist/index.cjs
CHANGED
|
@@ -1765,9 +1765,10 @@ var ModelAffinityRouter = class {
|
|
|
1765
1765
|
this.now = options.now ?? Date.now;
|
|
1766
1766
|
}
|
|
1767
1767
|
/**
|
|
1768
|
-
* Synchronously reorders `candidates` so
|
|
1768
|
+
* Synchronously reorders `candidates` so that, within each priority tier
|
|
1769
|
+
* (run of consecutive equal `priority`), endpoints whose fresh `/api/ps`
|
|
1769
1770
|
* snapshot shows `model` resident come first (relative order preserved within
|
|
1770
|
-
* each group). Returns the input unchanged — and kicks a background refresh —
|
|
1771
|
+
* each group). Never moves an endpoint across tiers. Returns the input unchanged — and kicks a background refresh —
|
|
1771
1772
|
* when any candidate's snapshot is missing or stale: a wrong reorder is worse
|
|
1772
1773
|
* than no reorder, and the first request after a cache miss simply isn't
|
|
1773
1774
|
* reordered. No-op without a `model` or with fewer than two candidates.
|
|
@@ -1784,13 +1785,28 @@ var ModelAffinityRouter = class {
|
|
|
1784
1785
|
}
|
|
1785
1786
|
}
|
|
1786
1787
|
if (!allFresh) return candidates;
|
|
1787
|
-
const
|
|
1788
|
+
const isResident = (ep) => {
|
|
1788
1789
|
const entry = this.cache.get(ep.name);
|
|
1789
1790
|
return entry !== void 0 && isModelResident(entry.models, model);
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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;
|
|
1794
1810
|
}
|
|
1795
1811
|
/**
|
|
1796
1812
|
* Forces a refresh of the affinity cache for the given endpoints (deduped with
|