@nemesis-oss/ollama-sdk 1.14.0 → 1.16.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 +42 -0
- package/README.md +131 -6
- package/dist/{anthropic-DrDUfbfQ.d.ts → anthropic-AKbXnWJU.d.cts} +1 -1
- package/dist/{anthropic-UeaVjLeP.d.cts → anthropic-CTv2acbj.d.ts} +1 -1
- package/dist/generated-api.d.cts +4 -4
- package/dist/generated-api.d.ts +4 -4
- package/dist/generated-runtime.cjs.map +1 -1
- package/dist/generated-runtime.d.cts +1 -1
- package/dist/generated-runtime.d.ts +1 -1
- package/dist/generated-runtime.js.map +1 -1
- package/dist/index.cjs +217 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +310 -5
- package/dist/index.d.ts +310 -5
- package/dist/index.js +213 -7
- package/dist/index.js.map +1 -1
- package/dist/mcp-generated.d.cts +1 -1
- package/dist/mcp-generated.d.ts +1 -1
- package/dist/{runtime-BIjr5ME4.d.ts → runtime-CFuv7Ifh.d.ts} +29 -1
- package/dist/{runtime-C4RRHqOs.d.cts → runtime-CdZP-N7j.d.cts} +29 -1
- package/dist/skills.cjs.map +1 -1
- package/dist/skills.js.map +1 -1
- package/dist/testing.cjs +13 -11
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +48 -8
- package/dist/testing.d.ts +48 -8
- package/dist/testing.js +13 -11
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.16.0] — Stream observation (`observeStream`), network-error classification & context-window recommendations
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
### Zero-copy stream observation (`observeStream`)
|
|
8
|
+
|
|
9
|
+
- **`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.
|
|
10
|
+
- 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).
|
|
11
|
+
- **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).
|
|
12
|
+
- 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/*`).
|
|
13
|
+
|
|
14
|
+
### Network-error classification (stale-socket recovery for every `fetch` shape)
|
|
15
|
+
|
|
16
|
+
- `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.
|
|
17
|
+
- 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.
|
|
18
|
+
- 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.
|
|
19
|
+
- 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.
|
|
20
|
+
|
|
21
|
+
### Context-window recommendations (the one nugget in a stale claim)
|
|
22
|
+
|
|
23
|
+
- `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`.
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
## [1.15.0] — Draft benchmark utility & request-aware mock routes: `compareDraftPerformance`, mock `responder`
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
### Speculative draft benchmark (`compareDraftPerformance`)
|
|
31
|
+
|
|
32
|
+
- **`compareDraftPerformance(client, options)`** — the deterministic A/B for "does `draft_num_predict` pay off for _this_ prompt": identical prompt on both sides, `temperature: 0` forced + a fixed `seed` (default 42), one warm-up generation per setting (pays the model load), `samples` measured runs per side (default 5) with the **median** compared — single samples mislead. Throughput flows through `formatTelemetry` (the official Usage-doc math; never hand-rolled nanosecond division).
|
|
33
|
+
- Result: `baselineTokensPerSec` / `speculativeTokensPerSec` (1dp), `speedupMultiplier` (2dp, computed from the two reported medians so the numbers are self-consistent), `effectiveGain` (`> gainThreshold`, default 1.05), plus full per-side `DraftRunStats` (every sample, medians).
|
|
34
|
+
- **Structural dependency** — accepts any `DraftBenchmarkClient` (`{ generate }` with the non-streaming signature); `OllamaClient` satisfies it without being named, and tests can pass a plain object. A `client.benchmark` namespace was deliberately not added, and the digest's `SpeculativeBenchmarkRunner` class was folded into the function — rationale in ADR 0030.
|
|
35
|
+
- Guarded: `RangeError` on invalid numeric options; non-positive/unusable samples are skipped, and a side with **no** valid samples throws `OllamaClientError` (`draft_benchmark_no_valid_samples`) instead of fabricating a zero/Infinity speedup; the abort signal is checked between runs and forwarded to every call.
|
|
36
|
+
- The no-draft-network reading is documented on the utility and in the README: a ≈1.0 speedup means the model has no draft network (and no embedded MTP tensors) — the server silently ignores the option.
|
|
37
|
+
|
|
38
|
+
### Request-aware mock routes (`responder`)
|
|
39
|
+
|
|
40
|
+
- `MockEndpointOptions` now accepts `responder(record) => MockRouteResponse | undefined` — per-request responses computed from the recorded method/path/body. Returning `undefined` falls back to the route's static fields, so fail-twice-then-succeed retry drills, body-dependent payloads (e.g. draft-setting-aware frames), and call-count scenarios are closure-local one-liners on the same engine — no subclassing, no one-shot global scenario queue. The digest's `ScenarioMockOllamaServer` sketch was declined for exactly that reason (it forks the request path ADR 0029 protects; see ADR 0030).
|
|
41
|
+
- The emission pipeline is shared: `chunkDelayMs`, `injectErrorFrame`, `dropConnectionMidStream`, and `rawBody` all keep their semantics inside responder routes; request recording and `onRequest` still fire.
|
|
42
|
+
- Type-level: the response fields moved to a new `MockRouteResponse` interface that `MockEndpointOptions` extends — pure refactor, existing routes unaffected. `MockRouteResponse` is exported from the `/testing` subpath.
|
|
43
|
+
- The SDK's own benchmark tests run daemonless through responder routes (draft-aware `/api/generate` frames) — the exact consumer CI pattern the digest's downstream stories describe, exercising the public engine rather than test-only fixtures.
|
|
44
|
+
|
|
3
45
|
## [1.14.0] — Speculative-decoding model assembly & the public testing subpath: `draftGguf`, `draft_num_predict` docs, `@nemesis-oss/ollama-sdk/testing`
|
|
4
46
|
|
|
5
47
|
The October 12 digest ("Speculative Decoding & Draft Model Creation") reviewed the official model-creation surfaces — `draft_files` + `draft_quantize` on `POST /api/create`, the `draft_num_predict` Modelfile parameter and runtime option — and asked for an exportable zero-network mock server. Audited against the codebase first (see [ADR 0029](./docs/adr/0029-public-testing-subpath-speculative-draft-conveniences.md)): the digest's headline typing claims were stale — `draft_files`/`draft_quantize` already shipped on both the generated `CreateRequest` and the hand-written `CreateRequestOptions` (official OpenAPI, 2026-10-07 pin), and `draft_num_predict` was already typed + documented on the runtime `ModelOptions`; the "DRAFT `<model>`" Modelfile instruction the digest describes is not in the current official docs (only the parameter is). The real gaps — a path-based draft upload convenience, wire tests, discoverable documentation, and the not-exported mock server — are closed here. All additive, no breaking changes. 932 pre-existing tests stay green, 28 new tests added.
|
package/README.md
CHANGED
|
@@ -34,7 +34,8 @@ Nexum’s model harness (2026-10-09) exercised this SDK via `gemma4:cloud` and `
|
|
|
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
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.
|
|
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
|
+
- 🧪 **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.
|
|
38
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.
|
|
39
40
|
- 🧹 **Client Teardown (`destroy()`)**: One call aborts every in-flight request, active stream, and queued capacity waiter with `OllamaAbortError` — the clean-exit path for `worker_threads`, CLI runners, and short-lived scripts where dangling fetch bodies keep the event loop alive.
|
|
40
41
|
- 🧠 **Dynamic Context-Window Discovery**: `client.models.getContextLength({ model })` resolves the real window from the server — the allocated window of the running instance (`/api/ps`), the Modelfile `num_ctx` default, and the native GGUF maximum (`model_info`), in precedence order — so applications stop hardcoding 2048/4096 guesses.
|
|
@@ -45,6 +46,7 @@ Nexum’s model harness (2026-10-09) exercised this SDK via `gemma4:cloud` and `
|
|
|
45
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.
|
|
46
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.
|
|
47
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.
|
|
48
50
|
- 📈 **OpenTelemetry Instrumentation**: Automatic spans for HTTP requests, endpoint failover, chat/generate calls, and agent runs — zero-cost when OpenTelemetry isn't installed.
|
|
49
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.
|
|
50
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.
|
|
@@ -351,7 +353,7 @@ const client = new OllamaClient({
|
|
|
351
353
|
// likely silently truncate …"
|
|
352
354
|
```
|
|
353
355
|
|
|
354
|
-
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`).
|
|
355
357
|
|
|
356
358
|
### Discovering the Real Context Window
|
|
357
359
|
|
|
@@ -533,9 +535,40 @@ console.log(`${t.tokensPerSecond} tok/s (load ${t.modelLoadMs}ms)`);
|
|
|
533
535
|
|
|
534
536
|
Already have the draft blobs uploaded? Pass the digest map directly —
|
|
535
537
|
`createModel({ draft_files: { 'draft.gguf': digest } })` — or merge both
|
|
536
|
-
(path uploads win on basename collision).
|
|
537
|
-
|
|
538
|
-
|
|
538
|
+
(path uploads win on basename collision).
|
|
539
|
+
|
|
540
|
+
**Does drafting actually pay off?** Upstream emits no draft-token
|
|
541
|
+
acceptance counter — speculative efficiency is only observable as effective
|
|
542
|
+
token throughput, and the signal is noisy: rejected draft predictions _add_
|
|
543
|
+
compute (high-temperature, creative prompts) while accepted drafts slash
|
|
544
|
+
`eval_duration` on predictable workloads (structured code, JSON extraction).
|
|
545
|
+
`compareDraftPerformance()` runs the deterministic A/B for a specific
|
|
546
|
+
prompt — identical prompt both sides, `temperature: 0` + a fixed seed, one
|
|
547
|
+
warm-up generation per setting (pays the model load), several samples per
|
|
548
|
+
side with the **median** compared, throughput derived by `formatTelemetry`:
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
import { OllamaClient, compareDraftPerformance } from '@nemesis-oss/ollama-sdk';
|
|
552
|
+
|
|
553
|
+
const comparison = await compareDraftPerformance(new OllamaClient(), {
|
|
554
|
+
model: 'my-model',
|
|
555
|
+
prompt: 'Extract the sender, subject, and date from this email as JSON: ...',
|
|
556
|
+
draftNumPredict: 4, // default; the baseline side always runs with 0
|
|
557
|
+
samples: 5, // median per side — single runs mislead
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
console.log(
|
|
561
|
+
`${comparison.baselineTokensPerSec} → ${comparison.speculativeTokensPerSec} tok/s ` +
|
|
562
|
+
`(${comparison.speedupMultiplier}x, gain: ${comparison.effectiveGain})`,
|
|
563
|
+
);
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
`effectiveGain` flips to `true` when the reported multiplier exceeds
|
|
567
|
+
`gainThreshold` (default `1.05` — five percent, past run-to-run noise). A
|
|
568
|
+
speedup of ≈1.0 is itself the diagnosis: the model has no draft network
|
|
569
|
+
(and no embedded MTP tensors), so the server silently ignores
|
|
570
|
+
`draft_num_predict` — the same reading `lab/15-speculative-draft/measure-draft-speedup.ts`
|
|
571
|
+
makes obvious across 0/4/8/16.
|
|
539
572
|
|
|
540
573
|
### Token Log Probabilities (`logprobs`)
|
|
541
574
|
|
|
@@ -942,6 +975,44 @@ export async function POST(req: Request) {
|
|
|
942
975
|
}
|
|
943
976
|
```
|
|
944
977
|
|
|
978
|
+
### Zero-Copy Stream Observation (multi-sink without `tee()`)
|
|
979
|
+
|
|
980
|
+
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.
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
import { observeStream } from '@nemesis-oss/ollama-sdk';
|
|
984
|
+
|
|
985
|
+
const stream = await client.chat({ model: 'llama4', messages, stream: true });
|
|
986
|
+
|
|
987
|
+
let tokens = 0;
|
|
988
|
+
const observed = observeStream(stream, {
|
|
989
|
+
// Sync or async — async observers run strictly FIFO, one at a time, and
|
|
990
|
+
// never delay the consumer below.
|
|
991
|
+
onChunk: (event) => {
|
|
992
|
+
if (event.type === 'token') {
|
|
993
|
+
tokens += 1;
|
|
994
|
+
auditLog.write(event.data.delta); // slow sink: throttles only itself
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
onDone: () => auditLog.flush(), // fires after the last chunk's sink settles
|
|
998
|
+
onAbandon: () => auditLog.markIncomplete(), // consumer broke early
|
|
999
|
+
onListenerError: (err, phase) => telemetry.count('audit_failure', { err, phase }),
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
for await (const event of observed) {
|
|
1003
|
+
ui.render(event); // primary consumer — full speed, full order
|
|
1004
|
+
}
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
The guarantees, in priority order:
|
|
1008
|
+
|
|
1009
|
+
- **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.
|
|
1010
|
+
- **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.
|
|
1011
|
+
- **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.
|
|
1012
|
+
- **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.
|
|
1013
|
+
|
|
1014
|
+
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.
|
|
1015
|
+
|
|
945
1016
|
### OpenAI & Anthropic Compatibility Bridges
|
|
946
1017
|
|
|
947
1018
|
```typescript
|
|
@@ -1552,12 +1623,40 @@ server.clearRequests(); // and server.clear() between tests
|
|
|
1552
1623
|
await server.stop(); // idempotent — safe in afterEach
|
|
1553
1624
|
```
|
|
1554
1625
|
|
|
1626
|
+
**Request-aware routes** — `responder` computes the response _per request_
|
|
1627
|
+
from the recorded method/path/body, covering the scenario drills a static
|
|
1628
|
+
route can't: fail-twice-then-succeed retries, body-dependent payloads,
|
|
1629
|
+
call-count behavior:
|
|
1630
|
+
|
|
1631
|
+
```typescript
|
|
1632
|
+
// Fail twice, then serve — retry/backoff drills:
|
|
1633
|
+
let hits = 0;
|
|
1634
|
+
server.register('/api/chat', {
|
|
1635
|
+
chunks: mockTextChunks('recovered'),
|
|
1636
|
+
responder: () =>
|
|
1637
|
+
hits++ < 2 ? { status: 503, chunks: ['{"error":"server queue is full"}'] } : undefined,
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// Body-dependent — answer differently by what the client sent:
|
|
1641
|
+
server.register('/api/generate', {
|
|
1642
|
+
responder: (record) => ({
|
|
1643
|
+
chunks: [frameFor(readDraftSetting(record.body))],
|
|
1644
|
+
}),
|
|
1645
|
+
});
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
Returning `undefined` falls back to the route's static fields; a returned
|
|
1649
|
+
response feeds the full emission pipeline, so `chunkDelayMs`,
|
|
1650
|
+
`injectErrorFrame`, `dropConnectionMidStream`, and `rawBody` all keep their
|
|
1651
|
+
semantics inside responder routes.
|
|
1652
|
+
|
|
1555
1653
|
Per-route controls: `status` (503/502/429 bodies), `chunkDelayMs`,
|
|
1556
1654
|
`dropConnectionMidStream` (connection reset mid-response), `onRequest`
|
|
1557
1655
|
(per-route body capture), and `injectErrorFrame.after` (error position).
|
|
1558
1656
|
Chunk builders: `mockChatChunk` (one frame with counter defaults),
|
|
1559
1657
|
`mockTextChunks` (chunked text + done frame), `mockToolCallChunks` (parallel
|
|
1560
|
-
tool calls + done frame
|
|
1658
|
+
tool calls + done frame — malformed-argument cases via string
|
|
1659
|
+
`arguments`). Node.js only — the subpath builds on `node:http`
|
|
1561
1660
|
and is never imported from the Edge-safe main entry.
|
|
1562
1661
|
|
|
1563
1662
|
### Edge Runtime Compatibility
|
|
@@ -1773,6 +1872,32 @@ favor of healthy endpoints for `cooldownMs`, and only used again — sorted soon
|
|
|
1773
1872
|
every endpoint is cooling down. Call `client.healthCheck()` or inspect the registry's `status()` to
|
|
1774
1873
|
observe per-endpoint circuit state directly.
|
|
1775
1874
|
|
|
1875
|
+
#### Stale sockets & automatic network-error recovery
|
|
1876
|
+
|
|
1877
|
+
Every SDK-routed request already runs through same-endpoint retry (default 3 attempts, jittered
|
|
1878
|
+
exponential backoff — tune or disable per client via `retries`) _and_ cross-endpoint failover. That
|
|
1879
|
+
covers the classic daemon-restart hazard: when Ollama is restarted or recycles idle keep-alive
|
|
1880
|
+
connections, a pooled socket dies mid-dispatch with `ECONNRESET`/`UND_ERR_SOCKET`, the request is
|
|
1881
|
+
re-dispatched on a fresh connection, and the caller never sees the reset — `list()`, `show()`,
|
|
1882
|
+
`ps()`, `version()`, and every other call alike.
|
|
1883
|
+
|
|
1884
|
+
What counts as a network failure is decided **structurally**, never by message text: `mapError`
|
|
1885
|
+
classifies WHATWG `fetch`'s `TypeError` rejections, any error carrying a `code` from
|
|
1886
|
+
`NETWORK_ERROR_CODES` (`ECONNRESET`, `ECONNREFUSED`, `EPIPE`, `ETIMEDOUT`, `EAI_AGAIN`,
|
|
1887
|
+
`UND_ERR_SOCKET`, `UND_ERR_CONNECT_TIMEOUT`, `UND_ERR_HEADERS_TIMEOUT`, `UND_ERR_BODY_TIMEOUT`,
|
|
1888
|
+
`UND_ERR_CLOSED`, …), and codes found nested in `cause` chains (bounded depth) — so custom `fetch`
|
|
1889
|
+
implementations built on `node:http`, whose socket errors aren't `TypeError`s, get the same
|
|
1890
|
+
automatic retry. The predicate (`isNetworkLevelError`) and the code set are exported for your own
|
|
1891
|
+
error handling. Two deliberate exclusions: `UND_ERR_ABORTED` stays an abort (retrying it would
|
|
1892
|
+
re-dispatch a request the caller explicitly cancelled), and non-transport codes like `EACCES` stay
|
|
1893
|
+
non-retryable (retrying a permission error just burns attempts).
|
|
1894
|
+
|
|
1895
|
+
A note on idempotency: the retry window is the pre-response phase — for the daemon-restart scenario
|
|
1896
|
+
the inference was lost server-side anyway, so re-dispatch does not double-charge. Requests that
|
|
1897
|
+
fail _mid-stream_ (after headers) are never auto-retried: partial generation can't be safely
|
|
1898
|
+
replayed. For a manual pre-flight when you'd rather probe than recover (e.g. right after a
|
|
1899
|
+
supervised daemon upgrade), `client.ping()` is the single-shot `HEAD /` liveness check.
|
|
1900
|
+
|
|
1776
1901
|
---
|
|
1777
1902
|
|
|
1778
1903
|
## Documentation
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable,
|
|
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,
|
|
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.
|
package/dist/generated-api.d.cts
CHANGED
|
@@ -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-
|
|
2
|
-
export { N as NativeApi } from './anthropic-
|
|
3
|
-
import { O as OllamaRuntime } from './runtime-
|
|
4
|
-
export { a as OperationDefinition } from './runtime-
|
|
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
|
|
package/dist/generated-api.d.ts
CHANGED
|
@@ -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-
|
|
2
|
-
export { N as NativeApi } from './anthropic-
|
|
3
|
-
import { O as OllamaRuntime } from './runtime-
|
|
4
|
-
export { a as OperationDefinition } from './runtime-
|
|
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
|
|