@nemesis-oss/ollama-sdk 1.20.1 → 1.21.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 +32 -8
- package/dist/index.cjs +47 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +47 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.21.0] — `systemOne` takes a cancellation signal; the failover layer stops dropping caller signals (ADR 0037)
|
|
4
|
+
|
|
5
|
+
The Decision Plane in nexum could not bound a System One call: its `AbortSignal` never reached the wire. Tracing that found a defect in the SDK, not in nexum. `FailoverHttpClient.request` replaced the caller's signal with the failover layer's own in every attempt, so a signal passed to any runtime operation that accepts one was silently ignored. [ADR 0037](./docs/adr/0037-systemone-signal-failover-cancellation.md) records the root cause and the declined alternatives.
|
|
6
|
+
|
|
7
|
+
### Fixed — per-call signals are honoured
|
|
8
|
+
|
|
9
|
+
- **`native.tags`, `native.ps`, `native.version`, `native.blobs`, and `runtime.invoke({ signal })`** now cancel in flight. They accepted the option and ignored it before this release. A caller that relied on the signal being ignored will now see an abort, and a caller that passes an already-aborted signal fails immediately without sending the request.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`client.systemOne(request, { signal })`.** The signal covers the whole call: the cloud-model version probe that runs first (ADR 0036), and the request on the wire. An abort rejects with `OllamaAbortError`. The probe is raced against the signal rather than cancelled, so one impatient caller cannot poison the cached probe for later calls.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `systemOne` calls `runtime.invoke` with the same operation and body that `NativeApi.systemOne` uses. Calls that pass no signal send the same request as before.
|
|
18
|
+
|
|
19
|
+
### Docs
|
|
20
|
+
|
|
21
|
+
- `docs-site/api/client.md` and `docs-site/guide/system-one.md` document the signal. ADR 0037 is added to both ADR indexes.
|
|
22
|
+
|
|
23
|
+
### Notes — CHANGELOG tidy
|
|
24
|
+
|
|
25
|
+
- Two `[Unreleased]` headings held entries that had already shipped. The 1.9.0-era entries (the IPv6 loopback fix, host normalization, and the digest work) now sit under 1.9.0, since their commits are in the `v1.9.0` tag. The MCP bridge and stdio entries now sit under "Releases 1.5.0 to 1.8.0", since their first commits are between `v1.5.0` and `v1.8.0`. There are still no release sections for 1.5.0, 1.5.1, or 1.6.0 in this file.
|
|
26
|
+
|
|
3
27
|
## [1.20.1] — Self-hosted servers are no longer refused as Ollama Cloud
|
|
4
28
|
|
|
5
29
|
Structured output (`format`) was refused pre-flight for every base URL outside `localhost`, `*.local`, `192.168.*` and `10.*`. The classifier treated any other host as cloud, so a self-hosted server reached by a Docker or Kubernetes service name (`http://ollama:11434`), a `172.16/12` address, or a public domain of its own threw `OllamaUnsupportedCapabilityError` on every `format` request. There was no opt-out. Caught while wiring `@nemesis-oss/ollama-sdk` into agentic-tui, whose docker-compose stack points the supervisor at `http://ollama:11434`.
|
|
@@ -420,18 +444,18 @@ A second full audit against the current official Ollama docs (`docs/openapi.yaml
|
|
|
420
444
|
- Five runnable examples: `examples/vision-inputs.ts`, `examples/responses-bridge.ts`, `examples/conversation-session.ts`, `examples/context-safety.ts`, `examples/blob-publishing.ts` (`npm run example <file>`) — plus `examples/usage-balance.ts` from the docs-recheck pass.
|
|
421
445
|
- 50 new unit tests (mocked fetch — CI-safe, no daemon): `test/vision-resolver.test.ts`, `test/responses-bridge.test.ts`, `test/conversation-session.test.ts`, `test/context-safety.test.ts`, `test/blob-helpers.test.ts` — plus 10 more in `test/usage-balance.test.ts`.
|
|
422
446
|
|
|
423
|
-
|
|
447
|
+
### Also shipped in 1.9.0 (filed under Unreleased)
|
|
424
448
|
|
|
425
|
-
|
|
449
|
+
#### Fix: IPv6 loopback was classified as Ollama Cloud
|
|
426
450
|
|
|
427
451
|
- `inferRuntimeMode()` compared `URL.hostname` to `'::1'`, but WHATWG URL reports an IPv6 literal with its brackets (`'[::1]'`), so `http://[::1]:11434` was classified as `cloud`. Structured-output (`format`) requests to a local Ollama on IPv6 loopback were therefore refused pre-flight with `OllamaUnsupportedCapabilityError`, and `localMode` was off for capability detection. The brackets are now stripped before comparing. Public IPv6 addresses are still `cloud`.
|
|
428
452
|
|
|
429
|
-
|
|
453
|
+
#### Host normalization and scoped `OLLAMA_API_KEY`
|
|
430
454
|
|
|
431
455
|
- **Base URL normalization.** `baseUrl`, `OLLAMA_HOST`, per-credential `baseUrl` and `endpoints[].baseUrl` now accept the bare forms the Ollama CLI accepts: `127.0.0.1:11434`, `0.0.0.0`, `:11434`, `myhost`. A missing scheme defaults to `http://`, a missing port on a scheme-less host defaults to `11434`, scheme-less port `443` selects `https://`, and `:port` means `127.0.0.1`. Full URLs and reverse-proxy path prefixes are unchanged. Previously a scheme-less value produced an invalid request URL.
|
|
432
456
|
- **Behavior change: `OLLAMA_API_KEY` is no longer forwarded to arbitrary hosts.** The env-sourced key is applied to the default endpoint only when its host is Ollama Cloud (`ollama.com`, `*.ollama.com`) or loopback. An explicit `apiKey` is always sent, to any host. If you relied on `OLLAMA_API_KEY` for a LAN or proxied server, pass `apiKey` explicitly. Web search/fetch (always `https://ollama.com`) still use the env key.
|
|
433
457
|
|
|
434
|
-
|
|
458
|
+
#### Production-readiness audit — tool-call accumulator, mock server
|
|
435
459
|
|
|
436
460
|
- **Offline wire-format conformance tests** (`test/conformance/native-api-offline.test.ts`). The sibling `native-api.test.ts` hits a REAL Ollama server and skips when none is available. The new offline tests use `OllamaMockServer` to emit contract-shaped NDJSON frames without any model inference, so they run in every CI environment — including those without an Ollama daemon. They validate ONLY the wire format (schema conformance, error-class mapping, optional-field handling), NOT model behavior. The split exists because wire-format drift is a contract concern (catchable offline), while model-behavior drift is a server/model concern (only catchable against a live server). 22 new tests covering:
|
|
437
461
|
- `GET /api/version`, `/api/tags`, `/api/ps`, `POST /api/show` schema conformance
|
|
@@ -443,7 +467,7 @@ A second full audit against the current official Ollama docs (`docs/openapi.yaml
|
|
|
443
467
|
- `OllamaClient` vs `NativeApi` shape parity (mirrors the live parity test)
|
|
444
468
|
- Updated `test/conformance/harness.ts` docstring to document the live-vs-offline split.
|
|
445
469
|
|
|
446
|
-
|
|
470
|
+
#### Production-readiness audit — tool-call accumulator, mock server
|
|
447
471
|
|
|
448
472
|
- **Defensive tool-call streaming accumulator (`src/tools/tool-call-accumulator.ts`).** The original `aggregateChat` in `src/streaming/normalize.ts` blindly appended every chunk's `tool_calls` array, which would produce duplicate entries if Ollama ever started streaming them incrementally. The new `ToolCallAccumulator` matches entries by `id` (the SDK-synthesized stable identifier) or by array position, and merges them with spread-semantics on `function.arguments`. For the documented native Ollama behavior (tool_calls arrive complete in one chunk), the accumulator is a no-op — one chunk carries the full array, the next chunk has nothing, and the accumulator appends exactly once. The accumulator is also exported for callers who want the same merge logic on raw NDJSON chunks.
|
|
449
473
|
- New exports: `ToolCallAccumulator` class, `mergeToolCallArrays()` stateless helper, `mergeToolCall()` / `mergeToolCallArgumentsString()` / `isSameToolCall()` standalone helpers.
|
|
@@ -460,7 +484,7 @@ A second full audit against the current official Ollama docs (`docs/openapi.yaml
|
|
|
460
484
|
- Idempotent `stop()` — safe to call from `afterEach` hooks without tracking whether `start()` ran.
|
|
461
485
|
- Ephemeral port support (`port: 0`) for parallel test runs.
|
|
462
486
|
|
|
463
|
-
|
|
487
|
+
#### Second digest — mid-stream errors, 502/503 specialization, pull-progress ergonomics
|
|
464
488
|
|
|
465
489
|
- **In-band stream-error trapping (NDJSON).** Ollama's streaming endpoints start with HTTP 200 OK and chunked transfer encoding; if generation fails mid-flight (GPU OOM, driver crash, context window overflow, model unload race), the server emits a final `{"error": "..."}` JSON frame and closes the stream. The HTTP status code never changes from 200, so HTTP-status-based error detection misses these errors entirely — they would silently bleed into the assistant's content stream as garbage tokens or undefined-field accesses.
|
|
466
490
|
- `parseNdjsonStream` now detects bare `{"error": "..."}` frames and throws `OllamaStreamError` immediately, with the error message preserved.
|
|
@@ -496,7 +520,7 @@ A second full audit against the current official Ollama docs (`docs/openapi.yaml
|
|
|
496
520
|
- Otherwise → `'unknown'` (caller should treat as "ask the model and see what it does").
|
|
497
521
|
- The family is extracted from `/api/show`'s `model_info.*.architecture` (with fallbacks to `details.family` then `details.families[0]`).
|
|
498
522
|
|
|
499
|
-
|
|
523
|
+
#### First digest — VRAM lifecycle, fetchWithBackoff, System One image caps, done_reason decoder
|
|
500
524
|
|
|
501
525
|
- **VRAM lifecycle primitives.** Added ergonomic helpers for explicit GPU memory management, addressing the upstream `keep_alive` semantics documented in the Ollama FAQ:
|
|
502
526
|
- `KeepAlive` type union (`string | number | 'unload' | 'indefinite'`) and `normalizeKeepAlive()` helper map the SDK-level sugar literals `'unload'` and `'indefinite'` to the wire-level `0` and `-1` sentinels.
|
|
@@ -554,7 +578,7 @@ A second full audit against the current official Ollama docs (`docs/openapi.yaml
|
|
|
554
578
|
|
|
555
579
|
- MCP tool arguments are now validated against advertised JSON Schema constraints before dispatch, while `resultMode: 'structured'` preserves raw MCP `CallToolResult` objects for programmatic consumers.
|
|
556
580
|
|
|
557
|
-
##
|
|
581
|
+
## Releases 1.5.0 to 1.8.0 (filed under Unreleased)
|
|
558
582
|
|
|
559
583
|
- **MCP bridge hardening.** MCP tool discovery now supports bounded pagination, repeated-cursor protection, request cancellation, richer MCP metadata types, and structured/non-text result preservation.
|
|
560
584
|
- **Optional Node MCP stdio adapter.** `@nemesis-oss/ollama-sdk/mcp/stdio` connects to local MCP servers using the official `@modelcontextprotocol/client` v2 transport without importing Node-only code into the root package.
|
package/dist/index.cjs
CHANGED
|
@@ -2626,6 +2626,24 @@ function createTimeoutSignal(timeoutMs, userSignal) {
|
|
|
2626
2626
|
};
|
|
2627
2627
|
return { signal: controller.signal, cancel };
|
|
2628
2628
|
}
|
|
2629
|
+
function untilAborted(promise, signal, message) {
|
|
2630
|
+
if (signal === void 0) return promise;
|
|
2631
|
+
return new Promise((resolve, reject) => {
|
|
2632
|
+
const onAbort = () => reject(new OllamaAbortError(message));
|
|
2633
|
+
promise.then(
|
|
2634
|
+
(value) => {
|
|
2635
|
+
signal.removeEventListener("abort", onAbort);
|
|
2636
|
+
resolve(value);
|
|
2637
|
+
},
|
|
2638
|
+
(error) => {
|
|
2639
|
+
signal.removeEventListener("abort", onAbort);
|
|
2640
|
+
reject(error);
|
|
2641
|
+
}
|
|
2642
|
+
);
|
|
2643
|
+
if (signal.aborted) onAbort();
|
|
2644
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2629
2647
|
|
|
2630
2648
|
// src/keep-alive.ts
|
|
2631
2649
|
var KEEP_ALIVE_UNLOAD = 0;
|
|
@@ -5653,15 +5671,13 @@ var FailoverHttpClient = class {
|
|
|
5653
5671
|
async request(options) {
|
|
5654
5672
|
const { model, ...httpOptions } = options;
|
|
5655
5673
|
return this.client.executeWithFailover(
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
};
|
|
5661
|
-
return http.request(mergedOptions);
|
|
5662
|
-
},
|
|
5674
|
+
// The signal handed to the operation already combines the caller's signal
|
|
5675
|
+
// (passed below) with the failover scope and timeout. Using it, rather than
|
|
5676
|
+
// the caller's, keeps the request cancellable by `destroy()` as well.
|
|
5677
|
+
async (http, signal) => http.request({ ...httpOptions, signal }),
|
|
5663
5678
|
{
|
|
5664
5679
|
singleEndpoint: false,
|
|
5680
|
+
...httpOptions.signal !== void 0 ? { signal: httpOptions.signal } : {},
|
|
5665
5681
|
...model !== void 0 ? { model } : {}
|
|
5666
5682
|
}
|
|
5667
5683
|
);
|
|
@@ -6501,12 +6517,18 @@ var OllamaClient = class _OllamaClient {
|
|
|
6501
6517
|
*
|
|
6502
6518
|
* For the low-level generated API (without the generic wrapper), use
|
|
6503
6519
|
* `client.native.systemOne(request)` directly.
|
|
6520
|
+
*
|
|
6521
|
+
* @param options.signal Cancels the whole call, including the cloud-model
|
|
6522
|
+
* version probe that runs first, so the gate never extends a caller's
|
|
6523
|
+
* deadline. An abort rejects with an {@link OllamaAbortError}.
|
|
6504
6524
|
*/
|
|
6505
|
-
async systemOne(request) {
|
|
6506
|
-
await this.assertCloudModelSupported(request.model);
|
|
6507
|
-
return this.
|
|
6508
|
-
|
|
6509
|
-
|
|
6525
|
+
async systemOne(request, options = {}) {
|
|
6526
|
+
await this.assertCloudModelSupported(request.model, options.signal);
|
|
6527
|
+
return this.runtime.invoke({
|
|
6528
|
+
operation: systemOneOp,
|
|
6529
|
+
body: request,
|
|
6530
|
+
...options.signal !== void 0 ? { signal: options.signal } : {}
|
|
6531
|
+
});
|
|
6510
6532
|
}
|
|
6511
6533
|
/**
|
|
6512
6534
|
* Cloud-model version gate for {@link systemOne} — throws
|
|
@@ -6518,19 +6540,25 @@ var OllamaClient = class _OllamaClient {
|
|
|
6518
6540
|
* so a later gated call retries), so at most one extra round-trip happens
|
|
6519
6541
|
* across all gated calls.
|
|
6520
6542
|
*/
|
|
6521
|
-
async assertCloudModelSupported(model) {
|
|
6543
|
+
async assertCloudModelSupported(model, signal) {
|
|
6522
6544
|
if (!isCloudModel(model)) return;
|
|
6523
6545
|
const candidates = this.registry.candidates();
|
|
6524
6546
|
const baseUrl = candidates[0]?.baseUrl ?? DEFAULT_BASE_URL;
|
|
6525
6547
|
if (inferRuntimeMode(baseUrl) !== "local") return;
|
|
6548
|
+
if (this.serverVersionCache === void 0) {
|
|
6549
|
+
this.serverVersionCache = this.native.version().then((v) => v.version);
|
|
6550
|
+
}
|
|
6551
|
+
const probe = this.serverVersionCache;
|
|
6526
6552
|
let version;
|
|
6527
6553
|
try {
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6554
|
+
version = await untilAborted(
|
|
6555
|
+
probe,
|
|
6556
|
+
signal,
|
|
6557
|
+
"System One call aborted while probing the Ollama server version"
|
|
6558
|
+
);
|
|
6559
|
+
} catch (error) {
|
|
6560
|
+
if (signal?.aborted) throw error;
|
|
6561
|
+
if (this.serverVersionCache === probe) this.serverVersionCache = void 0;
|
|
6534
6562
|
return;
|
|
6535
6563
|
}
|
|
6536
6564
|
if (version && compareOllamaVersions(version, CLOUD_MODEL_MIN_OLLAMA_VERSION) < 0) {
|