@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 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 *interchangeable* endpoints serving the same models — the fleet-of-workstations
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 *within* the runnable set:
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 *single-host* counterpart — per-model serial queues that deepen the already-hot
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 *full* cache
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 *where/when*, `formatTelemetry` for *how fast*).
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 endpoints whose fresh `/api/ps`
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 resident = candidates.filter((ep) => {
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
- if (resident.length === 0 || resident.length === candidates.length) return candidates;
1792
- const residentSet = new Set(resident.map((ep) => ep.name));
1793
- return [...resident, ...candidates.filter((ep) => !residentSet.has(ep.name))];
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