@nemesis-oss/ollama-sdk 1.15.0 → 1.17.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 CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.17.0] — Streaming blob uploads (`duplex:'half'`, two-pass file upload), typed-array view-safety pins, tracing-header verdicts
4
+
5
+ The October 14 digest ("Raw Model Blob Ingestion Streaming Backpressure, W3C Distributed Tracing Baggage Injection, Defensive Serialization for Typed Arrays") targeted heavy binary asset streaming, operational provenance, and low-level buffer safety. Audited against the codebase first (see [ADR 0032](./docs/adr/0032-streaming-blob-uploads-tracing-verdicts-view-safety.md)): the heap-exhaustion claim was real on the blob path — `createBlobFromFile` bulk-read the whole file (`fs.readFile`) before uploading, and the `ReadableStream` member of `BinaryBody` was a promise the transport couldn't keep (Node 18+ `fetch` rejects stream bodies without `duplex: 'half'`) — though the `/api/create` framing was misdirected (its request body is a small JSON digest map, not a stream). The tracing claim was stale (client-level `headers`, per-request headers, and the middleware seam already forward any W3C headers verbatim — nothing is stripped). The typed-array claim was stale as a defect (`src/` has zero `Buffer.from` call sites; the SDK's Web APIs are view-safe by construction, and adopting the proposed `Buffer`-coupled helpers would regress the edge-runtime zero-Node-builtins gate) — but the invariant is now pinned by tests. The companion upstream-specs digest (System One batching, payload caps, `expires_at`, model warmth) was comprehensively stale — every recommendation already ships. All additive, no breaking changes. 1012 pre-existing tests stay green, 12 new tests added.
6
+
7
+ ### Streaming blob uploads (MOD-01 — multi-GB GGUFs never enter the heap)
8
+
9
+ - **Transport**: both fetch call sites (middleware `finalHandler` and the direct path, previously duplicated init construction) now share one `buildFetchInit` that sets `duplex: 'half'` exactly when the body is a web `ReadableStream`. The guard is structural (`getReader` duck-typing) so cross-realm streams (workers, VM contexts) still get the member; the init type is a local `RequestInit & { duplex?: 'half' }` intersection because `@types/node` 22's `RequestInit` doesn't carry the member yet — assignable to the plain `RequestInit` `FetchLike` expects. Harmless (and spec'd) in browsers.
10
+ - **`createBlobFromStream(digest, stream, request?)`** — the multi-gigabyte-safe counterpart of `createBlobFromData`: pipes any web `ReadableStream<Uint8Array>` straight into the request body with HEAD-dedupe semantics identical to the data-based variant. The digest must be known up front — it _is_ the upload URL, and the server verifies streamed content against it, so a truncated or corrupted stream fails the upload instead of poisoning the store.
11
+ - **`createBlobFromFile` rewritten as two streaming passes** (same signature, same result shape): a SHA-256 hash pass through `node:crypto`'s incremental digest in 256 KiB chunks (skipped entirely when `opts.digest` is provided), then an upload pass piping `createReadStream` → `Readable.toWeb` into the streaming upload. Peak heap is O(chunk) in both passes; `createModelFromGguf` (which routes every shard through this method) inherits the bound. The method was already Node-only (hidden-specifier dynamic `node:fs`); the new `node:crypto`/`node:stream` imports follow the same pattern, keeping the main entry edge-portable.
12
+ - **Cancellation plumbing**: `createBlob`/`checkBlob` now accept `RequestCancellationOptions` (`signal`/`timeoutMs`) like their siblings, and `createBlobFromData`'s previously decorative `signal` option is actually wired through. Aborts surface as `OllamaAbortError` from both file passes (the hash pass checks between chunks and destroys the fd; the upload pass forwards the signal to `fetch` and destroys the stream on abort).
13
+ - New coverage pins the whole story: duplex injection (unit, injected `fetch`), byte-exact streamed uploads against a real capture server, the two-pass file flow (streaming digest matches the whole-file digest; wire bytes equal the file byte-for-byte), abort-before-traffic and abort-mid-stalled-upload, and daemonless public-mock round trips including `createModelFromGguf`'s headline flow (blob POST carries the shard, `/api/create` references it under its basename).
14
+
15
+ ### Typed-array view safety (BUF-01 — stale defect, pinned invariant)
16
+
17
+ - The audit found zero `Buffer.from(view.buffer)`-style call sites: `computeBlobDigest` uses Web Crypto, which hashes a `BufferSource` view's bytes per spec, and `fetch` serializes `Uint8Array` bodies per-view. The digest's `safeUint8ArrayToBuffer`/`toUint8Array` helpers were declined — they couple a runtime-portable SDK to Node's `Buffer` and would regress the edge-runtime gate (ADR 0032).
18
+ - The invariant is now pinned by the digest's own suggested probe, hardened with an external oracle: a `subarray(10, 20)` view of a 1024-byte patterned pool hashes to the standalone slice's digest (cross-checked against `node:crypto`) and arrives on the wire as exactly its 10 bytes — never the parent buffer.
19
+ - Cleanup: `computeBlobDigest`'s misleading `data as unknown as ArrayBuffer` cast removed (the local structural `SubtleCrypto` type already accepts `ArrayBufferView`).
20
+
21
+ ### Tracing headers (TRC-01 — stale, documented)
22
+
23
+ - The SDK forwards headers verbatim through three existing seams: client-level `headers` configuration (every request, every endpoint), per-request `headers` on transport request options (merged last, so they win), and middleware's mutable `RequestContext.headers` record — an OpenTelemetry-style extractor plugs in without touching a single method signature. The README's middleware section now shows the W3C `traceparent`/`tracestate`/`baggage` propagation pattern explicitly.
24
+ - The digest's `traceContext` option on every client method and ambient `AsyncLocalStorage` capture were declined (ADR 0032): signature inflation and hidden global state for capability that already exists.
25
+
26
+ ### Upstream-specs digest verdicts (all stale — pointers, no new API)
27
+
28
+ - **System One batching**: `client.systemOne()` already evaluates N named questions against shared state in one round trip, with compile-time question→answer type correlation the sketch lacks. **Payload caps**: `MAX_SYSTEM_ONE_REQUEST_BYTES` (64 KiB) / `MAX_SYSTEM_ONE_IMAGES_BYTES` (32 MiB) are exported, `estimateSystemOneRequestBytes()` is the pre-flight estimator, and the runtime enforces both client-side. **`expires_at`**: typed on `ModelResponse` from `/api/ps`, returned by `client.ps()`. **Model warmth**: `pin()`/`unload()`/`KEEP_ALIVE_INDEFINITE`/per-call `keep_alive`/`warmModelAffinity()` are the supported primitives; a background `ModelWarmthKeeper` timer daemon was declined (hidden event-loop work that escapes the client-teardown contract — reasoning in ADR 0032).
29
+
30
+ ## [1.16.0] — Stream observation (`observeStream`), network-error classification & context-window recommendations
31
+
32
+ The October 13 second digest ("Stream Multi-Sink Teeing, Context Window Overflow, Socket Hangup Recovery") targeted edge transport hazards and memory boundaries during high-throughput agent execution. Audited against the codebase first (see [ADR 0031](./docs/adr/0031-stream-observation-network-classification-context-recommendations.md)): the stream-teeing claim was the real gap (no observer utility existed, and `OllamaStream`'s dual modes are mutually exclusive — you couldn't tap a stream you were iterating); the context-overflow claim was comprehensively stale (a superior CJK-aware estimator with three-tier window resolution, `onContextOverflow` policies, and `getContextLength()` discovery has shipped since the v1.9.0 era — only its concrete-window suggestion was missing); and the socket-retry claim was inverted on the headline (every SDK-routed call already retries with backoff _and_ fails over — three retries where the digest asked for one) but real in one seam: network-error classification was `TypeError`-only, so code-carrying socket errors from custom `fetch` implementations and nested undici `cause` chains died un-retried on first attempt. All additive, no breaking changes. 977 pre-existing tests stay green, 35 new tests added.
33
+
34
+ ### Zero-copy stream observation (`observeStream`)
35
+
36
+ - **`observeStream(source, callbacks)`** — the safe multi-sink alternative to `ReadableStream.tee()`: one queue, no forked branch, no shared backpressure. Tap any `AsyncIterable` (streaming `chat`/`generate` streams, NDJSON chunk iterables, your own sources) for audit logging, token counting, or telemetry while the primary consumer iterates it.
37
+ - The contract is one-sided in favor of the primary consumer: observers **never block** (a serialized microtask chain — sync observers pay one hop, async observers run strictly FIFO with concurrency of exactly one, so a slow logger throttles only its own tail), **never fail the stream** (sync throws and async rejections route to `onListenerError` with the phase they came from — never unhandled rejections, never pipeline poisoning; a failing `onListenerError` is swallowed the same way), and **stay ordered** (`onDone` is enqueued only after the last chunk's observer settles, so an audit log has seen every chunk before its "done" marker).
38
+ - **Full lifecycle**: `onDone` on natural completion; `onError` + rethrow on source failure (the observer is transparent, not a circuit breaker); `onAbandon` when the consumer breaks early — firing after the iterator-close protocol has already closed the source. The digest's sketch (`void onChunk(chunk)`, no early-exit hook, fire-and-forget async observers) had exactly these three defects; the semantics are documented in ADR 0031 and pinned by adversarial tests (deferred resolution out of order, a never-resolving sink, `process`-level unhandled-rejection capture).
39
+ - Exported from the root entry alongside the rest of the streaming toolkit — a `./stream` subpath for a single function was declined (subpaths are reserved for distinct runtimes/concerns like `./testing` and `./mcp/*`).
40
+
41
+ ### Network-error classification (stale-socket recovery for every `fetch` shape)
42
+
43
+ - `mapError` now recognizes socket/connection-level failures **structurally**: any error carrying a `code` from the exported `NETWORK_ERROR_CODES` set (`ECONNRESET`, `ECONNREFUSED`, `EPIPE`, `ETIMEDOUT`, `EAI_AGAIN`, `UND_ERR_SOCKET`, `UND_ERR_CONNECT_TIMEOUT`, `UND_ERR_HEADERS_TIMEOUT`, `UND_ERR_BODY_TIMEOUT`, `UND_ERR_CLOSED`, …), including codes nested in `cause` chains (bounded depth, cycle-safe), maps to retryable + failover-eligible `OllamaNetworkError`. Previously only WHATWG `fetch`'s `TypeError` wrapper was classified — a custom `fetch` implementation built on `node:http` (whose `ECONNRESET` is a plain `Error` with a `code`) failed outright on first attempt with no retry and no failover. The digest's `String(err).includes(...)` matching was declined: message wording isn't a contract, `error.code` is.
44
+ - Two deliberate exclusions, both tested: `UND_ERR_ABORTED` stays an abort (retrying it would re-dispatch a request the caller explicitly cancelled), and non-transport codes like `EACCES` stay non-retryable.
45
+ - New e2e coverage pins the daemon-restart story end-to-end: a real `node:http` server that destroys the socket on first request per path — `list()` and `ps()` recover via re-dispatch and the caller never sees the reset; an injected `fetch` rejecting once with a raw `ECONNRESET` code retries exactly once and succeeds.
46
+ - The digest's "pre-flight health validation on stale sockets" was declined (undici owns the pool with no staleness introspection; the failed request is the cheapest detector) and its per-method idempotency gating was declined (the retry window is pre-response; mid-stream failures are never auto-retried) — reasoning in ADR 0031.
47
+
48
+ ### Context-window recommendations (the one nugget in a stale claim)
49
+
50
+ - `ContextCheck` gains `recommendedNumCtx` — present exactly when the threshold is exceeded — computed by the new exported `recommendContextWindow(estimatedTokens)`: the smallest power-of-two window covering `estimate × 1.3`, floored at 4096, via an exact doubling loop (float `Math.log2` of exact powers of two can round a perfect fit up a whole power of two). The pre-flight warning now names the concrete window **with the native-ceiling caveat** — verify the model's maximum via `client.models.getContextLength()` before adopting it, because a prompt that outgrows the native maximum needs compaction, not a bigger `num_ctx`.
51
+ - Non-finite/non-positive inputs degrade to the 4096 floor rather than throwing: the function is advisory, invoked after the caller's own estimate already flagged a problem.
52
+
3
53
  ## [1.15.0] — Draft benchmark utility & request-aware mock routes: `compareDraftPerformance`, mock `responder`
4
54
 
5
55
  The October 13 digest ("Speculative Performance & Telemetry Observations") reviewed how upstream reports speculative efficiency — no dedicated draft-token acceptance counter, throughput-only observability, a noisy signal (rejected drafts _add_ compute on creative prompts; accepted drafts slash `eval_duration` on structured ones) — and asked for a benchmark helper plus mock scenario hooks. Audited against the codebase first (see [ADR 0030](./docs/adr/0030-draft-benchmark-request-aware-mock-routes.md)): the "manual nanosecond ratios" claim was partially stale — `formatTelemetry` derives tokens/second since v1.12.0 and `lab/15` implements the full protocol, but neither reaches npm consumers (`files` publishes only `dist/`), so the packaged-harness gap was real. The mock scenario claims were stale: parallel tool calls in one frame, malformed-argument tool calls (string `arguments`), in-band mid-stream error injection, and 503 bodies all shipped in v1.14.0. The genuine engine gap the audit surfaced: routes couldn't vary responses **per request**. All additive, no breaking changes. 960 pre-existing tests stay green, 17 new tests added.
package/README.md CHANGED
@@ -33,7 +33,7 @@ Nexum’s model harness (2026-10-09) exercised this SDK via `gemma4:cloud` and `
33
33
  - 🌉 **OpenAI & Anthropic Compatibility Bridges**: Built-in clients for `/v1/chat/completions`, `/v1/responses`, `/v1/models`, and `/v1/messages`, including `reasoning_effort`/`reasoning.effort` for thinking models — plus an ergonomic dual-mode `client.responses.create()` bridge that prefers native `/v1/responses` and transparently re-issues via `/api/chat` on older servers (pre-v0.13.3).
34
34
  - 💬 **KV-Cache-Aware Conversation Sessions**: `client.session(model, systemPrompt)` keeps an append-only, prefix-stable history that maximizes Ollama's KV-cache reuse across turns, surfaces per-turn + cumulative cache hit rates (`prompt_eval_cached_count`) so cache degradation is visible instead of silent, and offers a one-call `compact()` sliding-window escape hatch when a session outgrows its window.
35
35
  - 🛑 **Context-Window Safety**: Heuristic client-side token estimation warns (or throws, `onContextOverflow: 'throw'`) before a request is sent when the prompt approaches the effective `num_ctx` window — Ollama's default behavior is to _silently truncate_ oversized prompts — and `defaultContextLength` makes the window explicit on every request.
36
- - 📦 **Blob & Custom Model Publishing**: Content-addressed blob management per Ollama's documented protocol — `computeBlobDigest()` (SHA-256 → `sha256:<hex>`), `createBlobFromData()`/`createBlobFromFile()` (HEAD-check + upload, skipping existing blobs), and one-shot `createModelFromGguf()` (upload GGUF shard blobs, then `/api/create` with `files: {name: digest}`) — with **speculative-decoding assembly**: `draftGguf` uploads the draft network's shards into `draft_files`, `draft_quantize` compresses them independently, and `parameters.draft_num_predict` tunes the per-step draft length.
36
+ - 📦 **Blob & Custom Model Publishing**: Content-addressed blob management per Ollama's documented protocol — `computeBlobDigest()` (SHA-256 → `sha256:<hex>`), `createBlobFromData()`/`createBlobFromFile()` (HEAD-check + upload, skipping existing blobs), **streaming uploads with O(chunk) heap** (`createBlobFromFile` hashes and uploads in 256 KiB chunks — multi-GB GGUFs never enter the Node heap; `createBlobFromStream()` pipes any web `ReadableStream` straight into `fetch` with `duplex: 'half'`), and one-shot `createModelFromGguf()` (upload GGUF shard blobs, then `/api/create` with `files: {name: digest}`) — with **speculative-decoding assembly**: `draftGguf` uploads the draft network's shards into `draft_files`, `draft_quantize` compresses them independently, and `parameters.draft_num_predict` tunes the per-step draft length.
37
37
  - 🏁 **Speculative Draft Benchmark (`compareDraftPerformance`)**: One call answers "does drafting pay off for _this_ prompt?" — identical prompt on both sides, deterministic sampling (`temperature: 0` + fixed seed), warm-up discard, several samples per side with **medians** compared, throughput via `formatTelemetry` — reported as baseline vs. speculative tok/s, `speedupMultiplier`, and `effectiveGain` past a configurable threshold. A ≈1.0 speedup is itself diagnostic: the model has no draft network, so the server silently ignores the option.
38
38
  - 🧪 **In-Memory Mock Server (`@nemesis-oss/ollama-sdk/testing`)**: The SDK's own deterministic test double, promoted public — `createMockOllamaServer()` with scriptable NDJSON routes, built-in liveness/version defaults (so `ping()` works out of the box), request recording for wire-level assertions, **request-aware routes** (`responder` — per-request responses for fail-twice-then-succeed retry drills and body-dependent payloads), mid-stream **in-band error injection** (`{"error": "..."}` frames at HTTP 200, exactly how a real daemon reports GPU OOM), connection drops, and correctly-shaped chunk builders for streaming text and **parallel tool calls** — zero network, zero daemon, CI-deterministic.
39
39
  - 🧵 **Batched Embeddings with Backpressure**: `client.embedBatch()` splits corpora of any size into bounded `/api/embed` batches (default 32 inputs × 3 in flight) — order-preserving, fail-fast, progress-reporting — so RAG/vector ingestion can't OOM a local daemon or saturate `OLLAMA_MAX_QUEUE` the way `Promise.all` floods do, with per-string context-window pre-flight against silent truncation.
@@ -46,6 +46,7 @@ Nexum’s model harness (2026-10-09) exercised this SDK via `gemma4:cloud` and `
46
46
  - 🩺 **Root Liveness Probe (`ping`)**: `client.ping()` is the official ultra-lightweight `HEAD /` health check — single-shot (no retry, no failover: the answer is about one concrete host), resolves instead of throwing, reports latency; `healthCheck()` remains the fleet-wide audit.
47
47
  - 🧼 **Vision History Hygiene**: `sanitizeHistoryForNextTurn()` evicts stale base64 image payloads from consumer-managed multi-turn vision histories — every `/api/chat` request re-sends the whole history, so old images silently bloat payload, context, and heap; the helper keeps images only on the trailing window you choose, conversation text fully preserved.
48
48
  - 🌊 **Web Stream Adapters**: Drop-in adapters (`toTextStream`, `toDataStream`, `toResponse`) for Next.js Route Handlers and Vercel AI SDK.
49
+ - 👀 **Zero-Copy Stream Observation (`observeStream`)**: The safe multi-sink alternative to `ReadableStream.tee()` — tap any async stream for audit logging, token counting, or telemetry while the primary consumer iterates it: observers run serialized FIFO on microtasks, a slow async sink (a logger waiting on I/O) never delays or backpressures the consumer, and observer throws/rejections route to an `onListenerError` channel instead of poisoning the pipeline or surfacing as unhandled rejections.
49
50
  - 📈 **OpenTelemetry Instrumentation**: Automatic spans for HTTP requests, endpoint failover, chat/generate calls, and agent runs — zero-cost when OpenTelemetry isn't installed.
50
51
  - 📊 **Client-Side Quota Monitoring**: `QuotaManager` tracks token/request usage against budgets you configure across rolling windows (e.g. Ollama Cloud's 5-hour session / 7-day weekly resets) and fails fast with `OllamaQuotaExceededError` before a request is sent.
51
52
  - ⚡ **Edge Runtime Verified**: CI bundles and runs the client in a real Edge Runtime sandbox (Cloudflare Workers/Vercel Edge-compatible) with zero Node.js APIs.
@@ -352,7 +353,7 @@ const client = new OllamaClient({
352
353
  // likely silently truncate …"
353
354
  ```
354
355
 
355
- The estimators are exported for custom pipelines: `estimateTokens(text)` (CJK-aware — ~1 token per CJK character, ~4 chars/token elsewhere), `estimateChatRequestTokens(req)` / `estimateGenerateRequestTokens(req)` (message overhead + tool schemas + `IMAGE_TOKEN_ESTIMATE` per image), and the underlying `checkChatContext` / `checkGenerateContext` / `contextWarningMessage`. All are heuristics (±20–30%) — for exact counts, ask the server (`prompt_eval_count`).
356
+ The estimators are exported for custom pipelines: `estimateTokens(text)` (CJK-aware — ~1 token per CJK character, ~4 chars/token elsewhere), `estimateChatRequestTokens(req)` / `estimateGenerateRequestTokens(req)` (message overhead + tool schemas + `IMAGE_TOKEN_ESTIMATE` per image), and the underlying `checkChatContext` / `checkGenerateContext` / `contextWarningMessage`. Every failing check now carries a concrete `recommendedNumCtx` — the smallest power-of-two window with ~30% headroom (via the exported `recommendContextWindow(estimatedTokens)`) — and the warning says so explicitly, with the caveat to verify the model's native ceiling via `getContextLength()` before adopting it. All are heuristics (±20–30%) — for exact counts, ask the server (`prompt_eval_count`).
356
357
 
357
358
  ### Discovering the Real Context Window
358
359
 
@@ -499,6 +500,34 @@ await client.models.createModelFromGguf('my-model', [
499
500
  ]);
500
501
  ```
501
502
 
503
+ #### Streaming uploads — multi-gigabyte weights with O(chunk) heap
504
+
505
+ Both `createBlobFromFile` and `createModelFromGguf` (which routes every shard
506
+ through it) stream instead of bulk-reading: the SHA-256 hash pass and the
507
+ upload pass each consume the file in 256 KiB chunks, so peak heap is a chunk,
508
+ never the file. Pass `digest` when you already know it to skip the hash pass
509
+ entirely. When you own the bytes (a chunk producer, a WASM/SharedArrayBuffer
510
+ pipeline, a resumed download), `createBlobFromStream` pipes any web
511
+ `ReadableStream<Uint8Array>` straight into the request body — the transport
512
+ sets `duplex: 'half'` (required by Node 18+ `fetch` for stream bodies)
513
+ automatically, and the server verifies the streamed content against the
514
+ digest, so a truncated stream fails the upload instead of corrupting the
515
+ store:
516
+
517
+ ```typescript
518
+ // digest known up front (hash first when it isn't — the digest IS the URL)
519
+ const { alreadyExisted } = await client.models.createBlobFromStream(
520
+ 'sha256:…',
521
+ myChunkProducer, // ReadableStream<Uint8Array>
522
+ );
523
+
524
+ // Both methods accept { signal, timeoutMs } for cancellation/timeout:
525
+ await client.models.createBlobFromFile('/models/model.gguf', {
526
+ digest: 'sha256:…', // optional: skip the hash pass
527
+ signal: controller.signal,
528
+ });
529
+ ```
530
+
502
531
  ### Speculative Decoding (Draft Models)
503
532
 
504
533
  Ollama packages speculative decoding as a small **draft network** whose token
@@ -974,6 +1003,44 @@ export async function POST(req: Request) {
974
1003
  }
975
1004
  ```
976
1005
 
1006
+ ### Zero-Copy Stream Observation (multi-sink without `tee()`)
1007
+
1008
+ Feeding the same stream to a UI _and_ a logger (token counter, audit trail, telemetry exporter) is where `ReadableStream.tee()` bites: both branches share one upstream pull, so a slow branch — a logger waiting on a database write — forces every intermediate chunk to be queued in that branch, throttles the fast branch through shared backpressure, and leaks heap if the slow sink stalls. `observeStream` is the single-pass alternative: one queue, no fork, and a contract that is one-sided in favor of the primary consumer.
1009
+
1010
+ ```typescript
1011
+ import { observeStream } from '@nemesis-oss/ollama-sdk';
1012
+
1013
+ const stream = await client.chat({ model: 'llama4', messages, stream: true });
1014
+
1015
+ let tokens = 0;
1016
+ const observed = observeStream(stream, {
1017
+ // Sync or async — async observers run strictly FIFO, one at a time, and
1018
+ // never delay the consumer below.
1019
+ onChunk: (event) => {
1020
+ if (event.type === 'token') {
1021
+ tokens += 1;
1022
+ auditLog.write(event.data.delta); // slow sink: throttles only itself
1023
+ }
1024
+ },
1025
+ onDone: () => auditLog.flush(), // fires after the last chunk's sink settles
1026
+ onAbandon: () => auditLog.markIncomplete(), // consumer broke early
1027
+ onListenerError: (err, phase) => telemetry.count('audit_failure', { err, phase }),
1028
+ });
1029
+
1030
+ for await (const event of observed) {
1031
+ ui.render(event); // primary consumer — full speed, full order
1032
+ }
1033
+ ```
1034
+
1035
+ The guarantees, in priority order:
1036
+
1037
+ - **Never blocks.** Observer callbacks run on a serialized microtask chain off the hot path. A slow async observer delays only its own tail; the consumer receives chunks at source speed.
1038
+ - **Never fails.** A sync throw or async rejection in any observer routes to `onListenerError` (with the phase it came from: `chunk` / `done` / `error` / `abandoned`) and is otherwise discarded — observer failures can never poison the pipeline or surface as unhandled rejections. A failing `onListenerError` itself is swallowed the same way.
1039
+ - **Ordered.** Observers run strictly FIFO with concurrency of exactly one, and `onDone` is enqueued only after the last chunk's observer settles — the audit log has seen every chunk before its "done" marker.
1040
+ - **Full lifecycle.** Natural completion fires `onDone`; a source error fires `onError` and re-throws to the consumer (the observer is transparent, not a circuit breaker); an early `break` in the consuming loop fires `onAbandon` — after the source has already been closed by the iterator-`return()` protocol.
1041
+
1042
+ Works over any `AsyncIterable<T>` — `OllamaStream` from streaming `chat`/`generate`, raw NDJSON chunk iterables, or your own sources. (Note `OllamaStream` itself is dual-mode but mode-exclusive — `.on()` listeners can't be combined with `for await`; `observeStream` is the supported way to tap a stream you're already iterating.) Stopping early is a plain `break`; if you need programmatic cancellation of an SDK stream, keep a handle on the original stream's `abort()`. The only state the wrapper retains is one closure per backlogged observer callback — if your sink can stall _forever_, bound it on your side (queue, drop policy), exactly as you would any fire-and-forget log sink.
1043
+
977
1044
  ### OpenAI & Anthropic Compatibility Bridges
978
1045
 
979
1046
  ```typescript
@@ -1833,6 +1900,32 @@ favor of healthy endpoints for `cooldownMs`, and only used again — sorted soon
1833
1900
  every endpoint is cooling down. Call `client.healthCheck()` or inspect the registry's `status()` to
1834
1901
  observe per-endpoint circuit state directly.
1835
1902
 
1903
+ #### Stale sockets & automatic network-error recovery
1904
+
1905
+ Every SDK-routed request already runs through same-endpoint retry (default 3 attempts, jittered
1906
+ exponential backoff — tune or disable per client via `retries`) _and_ cross-endpoint failover. That
1907
+ covers the classic daemon-restart hazard: when Ollama is restarted or recycles idle keep-alive
1908
+ connections, a pooled socket dies mid-dispatch with `ECONNRESET`/`UND_ERR_SOCKET`, the request is
1909
+ re-dispatched on a fresh connection, and the caller never sees the reset — `list()`, `show()`,
1910
+ `ps()`, `version()`, and every other call alike.
1911
+
1912
+ What counts as a network failure is decided **structurally**, never by message text: `mapError`
1913
+ classifies WHATWG `fetch`'s `TypeError` rejections, any error carrying a `code` from
1914
+ `NETWORK_ERROR_CODES` (`ECONNRESET`, `ECONNREFUSED`, `EPIPE`, `ETIMEDOUT`, `EAI_AGAIN`,
1915
+ `UND_ERR_SOCKET`, `UND_ERR_CONNECT_TIMEOUT`, `UND_ERR_HEADERS_TIMEOUT`, `UND_ERR_BODY_TIMEOUT`,
1916
+ `UND_ERR_CLOSED`, …), and codes found nested in `cause` chains (bounded depth) — so custom `fetch`
1917
+ implementations built on `node:http`, whose socket errors aren't `TypeError`s, get the same
1918
+ automatic retry. The predicate (`isNetworkLevelError`) and the code set are exported for your own
1919
+ error handling. Two deliberate exclusions: `UND_ERR_ABORTED` stays an abort (retrying it would
1920
+ re-dispatch a request the caller explicitly cancelled), and non-transport codes like `EACCES` stay
1921
+ non-retryable (retrying a permission error just burns attempts).
1922
+
1923
+ A note on idempotency: the retry window is the pre-response phase — for the daemon-restart scenario
1924
+ the inference was lost server-side anyway, so re-dispatch does not double-charge. Requests that
1925
+ fail _mid-stream_ (after headers) are never auto-retried: partial generation can't be safely
1926
+ replayed. For a manual pre-flight when you'd rather probe than recover (e.g. right after a
1927
+ supervised daemon upgrade), `client.ping()` is the single-shot `HEAD /` liveness check.
1928
+
1836
1929
  ---
1837
1930
 
1838
1931
  ## Documentation
@@ -1869,6 +1962,25 @@ around the underlying HTTP request for native endpoints, OpenAI/Anthropic compat
1869
1962
  model health checks, and the hosted web tools. Lifecycle events expose `start`, `success`,
1870
1963
  `retry`, and `error` events with a request id, timing, and status/error information.
1871
1964
 
1965
+ **Distributed tracing (W3C `traceparent`/`tracestate`/`baggage`)**: the SDK forwards
1966
+ whatever headers it is given and never strips or rewrites yours — three seams cover
1967
+ propagation. Client-wide (every request, every endpoint):
1968
+
1969
+ ```ts
1970
+ const client = new OllamaClient({
1971
+ headers: {
1972
+ traceparent: carrierHeader('traceparent'), // from your inbound request
1973
+ baggage: carrierHeader('baggage'),
1974
+ },
1975
+ });
1976
+ ```
1977
+
1978
+ Per-request or computed (the middleware seam from above — `request.headers` is a mutable
1979
+ record, so an OpenTelemetry-style extractor plugs in directly and can vary headers per
1980
+ call). A `traceContext` option on every SDK method was considered and declined
1981
+ (ADR 0032): it would inflate every signature for something these seams already do
1982
+ without touching them.
1983
+
1872
1984
  ```ts
1873
1985
  const client = new OllamaClient({
1874
1986
  middleware: [
@@ -1,4 +1,4 @@
1
- import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, af as SseEvent, y as OllamaClientError } from './runtime-BIjr5ME4.js';
1
+ import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, ag as SseEvent, z as OllamaClientError } from './runtime-CdZP-N7j.cjs';
2
2
 
3
3
  /**
4
4
  * AUTO-GENERATED by `npm run contract:generate`. DO NOT EDIT.
@@ -1,4 +1,4 @@
1
- import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, af as SseEvent, y as OllamaClientError } from './runtime-C4RRHqOs.cjs';
1
+ import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, ag as SseEvent, z as OllamaClientError } from './runtime-CFuv7Ifh.js';
2
2
 
3
3
  /**
4
4
  * AUTO-GENERATED by `npm run contract:generate`. DO NOT EDIT.
@@ -1,7 +1,7 @@
1
- import { O as OpenAIChatCompletionRequest, a as OpenAIChatCompletionResponse, b as OpenAIChatCompletionChunk, c as OpenAICompletionRequest, d as OpenAICompletionResponse, e as OpenAICompletionChunk, f as OpenAIEmbeddingRequest, g as OpenAIEmbeddingResponse, h as OpenAIListModelsResponse, i as OpenAIModelItem, j as OpenAIResponsesRequest, k as OpenAIResponsesResponse, l as OpenAIResponsesStreamEvent, A as AnthropicMessagesRequest, m as AnthropicMessagesResponse, n as AnthropicMessageStreamEvent } from './anthropic-UeaVjLeP.cjs';
2
- export { N as NativeApi } from './anthropic-UeaVjLeP.cjs';
3
- import { O as OllamaRuntime } from './runtime-C4RRHqOs.cjs';
4
- export { a as OperationDefinition } from './runtime-C4RRHqOs.cjs';
1
+ import { O as OpenAIChatCompletionRequest, a as OpenAIChatCompletionResponse, b as OpenAIChatCompletionChunk, c as OpenAICompletionRequest, d as OpenAICompletionResponse, e as OpenAICompletionChunk, f as OpenAIEmbeddingRequest, g as OpenAIEmbeddingResponse, h as OpenAIListModelsResponse, i as OpenAIModelItem, j as OpenAIResponsesRequest, k as OpenAIResponsesResponse, l as OpenAIResponsesStreamEvent, A as AnthropicMessagesRequest, m as AnthropicMessagesResponse, n as AnthropicMessageStreamEvent } from './anthropic-AKbXnWJU.cjs';
2
+ export { N as NativeApi } from './anthropic-AKbXnWJU.cjs';
3
+ import { O as OllamaRuntime } from './runtime-CdZP-N7j.cjs';
4
+ export { a as OperationDefinition } from './runtime-CdZP-N7j.cjs';
5
5
  import './types-DbPnfVh-.cjs';
6
6
  import 'zod';
7
7
 
@@ -1,7 +1,7 @@
1
- import { O as OpenAIChatCompletionRequest, a as OpenAIChatCompletionResponse, b as OpenAIChatCompletionChunk, c as OpenAICompletionRequest, d as OpenAICompletionResponse, e as OpenAICompletionChunk, f as OpenAIEmbeddingRequest, g as OpenAIEmbeddingResponse, h as OpenAIListModelsResponse, i as OpenAIModelItem, j as OpenAIResponsesRequest, k as OpenAIResponsesResponse, l as OpenAIResponsesStreamEvent, A as AnthropicMessagesRequest, m as AnthropicMessagesResponse, n as AnthropicMessageStreamEvent } from './anthropic-DrDUfbfQ.js';
2
- export { N as NativeApi } from './anthropic-DrDUfbfQ.js';
3
- import { O as OllamaRuntime } from './runtime-BIjr5ME4.js';
4
- export { a as OperationDefinition } from './runtime-BIjr5ME4.js';
1
+ import { O as OpenAIChatCompletionRequest, a as OpenAIChatCompletionResponse, b as OpenAIChatCompletionChunk, c as OpenAICompletionRequest, d as OpenAICompletionResponse, e as OpenAICompletionChunk, f as OpenAIEmbeddingRequest, g as OpenAIEmbeddingResponse, h as OpenAIListModelsResponse, i as OpenAIModelItem, j as OpenAIResponsesRequest, k as OpenAIResponsesResponse, l as OpenAIResponsesStreamEvent, A as AnthropicMessagesRequest, m as AnthropicMessagesResponse, n as AnthropicMessageStreamEvent } from './anthropic-CTv2acbj.js';
2
+ export { N as NativeApi } from './anthropic-CTv2acbj.js';
3
+ import { O as OllamaRuntime } from './runtime-CFuv7Ifh.js';
4
+ export { a as OperationDefinition } from './runtime-CFuv7Ifh.js';
5
5
  import './types-DbPnfVh-.js';
6
6
  import 'zod';
7
7