@nemesis-oss/ollama-sdk 1.8.0 → 1.10.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/README.md +247 -11
  3. package/dist/{anthropic-B_UXplNF.d.ts → anthropic-CPaoxeLX.d.ts} +4 -2
  4. package/dist/{anthropic-CVDFy_a0.d.cts → anthropic-DuE8M6Tz.d.cts} +4 -2
  5. package/dist/{compose-DLStUfaF.d.ts → compose-BQYXdpar.d.ts} +1 -1
  6. package/dist/{compose-DS-UWFx8.d.cts → compose-lt-Qz3lI.d.cts} +1 -1
  7. package/dist/generated-api.cjs +42 -3
  8. package/dist/generated-api.cjs.map +1 -1
  9. package/dist/generated-api.d.cts +96 -6
  10. package/dist/generated-api.d.ts +96 -6
  11. package/dist/generated-api.js +41 -4
  12. package/dist/generated-api.js.map +1 -1
  13. package/dist/generated-models-schemas.cjs +62 -2
  14. package/dist/generated-models-schemas.cjs.map +1 -1
  15. package/dist/generated-models-schemas.d.cts +108 -3
  16. package/dist/generated-models-schemas.d.ts +108 -3
  17. package/dist/generated-models-schemas.js +56 -3
  18. package/dist/generated-models-schemas.js.map +1 -1
  19. package/dist/generated-runtime.cjs +105 -5
  20. package/dist/generated-runtime.cjs.map +1 -1
  21. package/dist/generated-runtime.d.cts +2 -2
  22. package/dist/generated-runtime.d.ts +2 -2
  23. package/dist/generated-runtime.js +105 -5
  24. package/dist/generated-runtime.js.map +1 -1
  25. package/dist/index.cjs +1644 -54
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +1506 -14
  28. package/dist/index.d.ts +1506 -14
  29. package/dist/index.js +1607 -55
  30. package/dist/index.js.map +1 -1
  31. package/dist/mcp/tools.json +43 -24
  32. package/dist/mcp-generated.cjs +43 -1
  33. package/dist/mcp-generated.cjs.map +1 -1
  34. package/dist/mcp-generated.d.cts +2 -2
  35. package/dist/mcp-generated.d.ts +2 -2
  36. package/dist/mcp-generated.js +43 -1
  37. package/dist/mcp-generated.js.map +1 -1
  38. package/dist/{runtime-C6UYtBRB.d.ts → runtime-BV9SK_cf.d.ts} +69 -2
  39. package/dist/{runtime-DTmUeU6K.d.cts → runtime-BsdBbuOv.d.cts} +69 -2
  40. package/dist/skills.cjs.map +1 -1
  41. package/dist/skills.d.cts +3 -3
  42. package/dist/skills.d.ts +3 -3
  43. package/dist/skills.js.map +1 -1
  44. package/dist/{types-5PAjLoCp.d.cts → types-BtQexFpT.d.cts} +113 -10
  45. package/dist/{types-5PAjLoCp.d.ts → types-BtQexFpT.d.ts} +113 -10
  46. package/package.json +11 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,187 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.0] — Production-readiness audit: batched embedding pipeline, client teardown, type-declaration hardening
4
+
5
+ Three findings from the daily production-readiness audit (see [ADR 0025](./docs/adr/0025-embedding-batching-client-teardown-declaration-portability.md)) — all additive, no breaking changes; 749 pre-existing tests stay green, 29 new tests added.
6
+
7
+ ### Batch-constrained embedding pipeline (`src/embed-batch.ts` + `client.embedBatch()`) — EMB-01
8
+
9
+ - **`client.embedBatch({ model, input, batchSize?, concurrency?, … })`** for high-volume RAG/vector ingestion: splits any corpus into bounded `/api/embed` batches (defaults: 32 inputs × 3 in flight) instead of `Promise.all` floods that OOM local daemons, contend sockets, and saturate `OLLAMA_MAX_QUEUE` (503s).
10
+ - **Order-preserving** — `embeddings[i]` always corresponds to `input[i]` regardless of batch completion order; safe to zip straight into a vector store.
11
+ - **Fail-fast** — the first batch error aborts its in-flight siblings (each still riding the client's full failover/retry/auth/telemetry pipeline) and rejects the whole operation with the _original_ error, so a half-indexed corpus never looks like success. Caller `signal` cancels queued and in-flight batches alike (`code: 'aborted'`).
12
+ - **Per-string context-window pre-flight** — Ollama _silently truncates_ embedding inputs longer than `num_ctx`; `embedBatch()` injects `defaultContextLength` as `num_ctx` and warns (or throws under `onContextOverflow: 'throw'`) with the offending indexes _before_ any request is sent. No margin, unlike chat/generate: an embedding prompt is the whole input.
13
+ - `onBatchComplete(done, total)` progress callback; `truncate`/`dimensions`/`keep_alive`/`options`/`timeoutMs` pass through per batch; new exports `batchEmbed`, `findOversizedEmbedInputs`, `embedBatchOverflowMessage`, `DEFAULT_EMBED_BATCH_SIZE`, `DEFAULT_EMBED_CONCURRENCY` + types.
14
+
15
+ ### Client teardown — `OllamaClient.destroy(reason)` — THD-01
16
+
17
+ - One call aborts **every in-flight request, active stream, and queued capacity waiter** with `OllamaAbortError` (`code: 'aborted'`, message = `reason`) — the clean-exit path for `node:worker_threads`, CLI runners, and short-lived processes where dangling fetch bodies and unconsumed stream readers keep the event loop alive.
18
+ - Streams reject their `finalResult` and release their `maxConcurrentPerEndpoint` slots via the existing `holdUntil` release path; scopes deregister the moment their work settles, so `destroy()` can tear down a stream mid-consumption but never outlives finished work.
19
+ - Idempotent (repeat calls return `0`) and a drain, not a disable — the client stays usable afterward. Returns the number of aborted operations. Covers every surface that goes through `executeWithFailover`/`executeCloudRequest`: chat/generate/embed (+Batch), models, responses bridge, sessions, agent, usage/balance, web tools.
20
+
21
+ ### Type-declaration portability hardening — TYP-03
22
+
23
+ - Verified (not just assumed) the dual-packaging story: `@arethetypeswrong/cli` reports zero problems across node10/node16-CJS/node16-ESM/bundler for every entry point, and the exports map already maps `import → .d.ts` / `require → .d.cts` per subpath.
24
+ - **New `scripts/verify-consumer-types.sh`** (also in CI and `verify:release`): compiles two _real_ consumer projects against the packed tarball — a CommonJS consumer under `--module node16` (the `require` condition, exactly the TS1479 "masquerading ESM types" scenario) and an ESM consumer under `--module nodenext`.
25
+ - **Fixed a real leak it caught**: `VisionInput` included Node's `Buffer` global, so strict consumers without `@types/node` could not compile against our declarations. Now `string | Uint8Array` — Node Buffers are `Uint8Array` subclasses and remain fully accepted (13 vision tests untouched and green) — the published declarations are free of Node ambient globals.
26
+
27
+ ## [1.9.0] — Digest upgrades: vision resolver, dual-mode Responses bridge, KV-cache sessions, context safety, blob publishing, cloud usage & balance
28
+
29
+ Five upgrades closing the gaps surfaced by the official Ollama documentation review (see [ADR 0024](./docs/adr/0024-digest-upgrades-vision-responses-sessions-context-blobs.md)) — plus a second-pass recheck against the current official docs (see the last section) that added the Ollama Cloud account endpoints and a missing `/api/create` field. All additive — no breaking changes; 689 pre-existing tests stay green, 60 new tests added.
30
+
31
+ ### Universal vision asset resolver (`src/vision.ts`)
32
+
33
+ - **`VisionInput = string | Buffer | Uint8Array`** accepted anywhere `images` is (`Message.images`, `generate()`), implementing the official SDK vision convention: SDKs accept polymorphic sources; the REST API strictly requires raw base64 strings.
34
+ - String resolution order: `data:image/...;base64,` data URIs (header stripped) → `http(s)://` URLs (fetched via global `fetch`, abortable through the request's `signal`) → image-extension file paths (`.png`/`.jpg`/`.webp`/…, read via **dynamically imported** `node:fs` — Node only, browser bundles unaffected; graceful passthrough when the read fails) → assume raw base64.
35
+ - Path detection is extension-based, not slash-based — `/` is a legal base64 alphabet character, and a contains-slash heuristic would misfire on long payloads.
36
+ - The request pipeline stays allocation-free when every entry is already base64 (`withEncodedImages` returns the same array reference).
37
+ - Previously-broken inputs are silently fixed: data-URI strings, which used to ship the `data:image/...;base64,` header to the server, are now stripped.
38
+ - New exports: `resolveImageInput()`, `imageStringNeedsResolution()`, `resolveImages()`, `type VisionInput`.
39
+
40
+ ### Dual-mode OpenAI Responses bridge (`src/responses.ts`)
41
+
42
+ - **`client.responses.create()`** — ergonomic, OpenAI-SDK-shaped bridge (`model`, `input`, `instructions`, `tools`, `temperature`, `top_p`, `max_output_tokens`, `reasoning_effort`, `think`) over `POST /v1/responses` (Ollama ≥ v0.13.3), with **transparent `/api/chat` fallback on 404** for older servers: `instructions` → system message, `max_output_tokens` → `options.num_predict`, reply normalized back into the Responses shape (`output_text`, `usage.input_tokens`/`output_tokens`). The result's `transport` field (`'native' | 'chat-adapter'`) records the path taken.
43
+ - **`client.responses.stream()`** — lazy async generator yielding simplified `{type: 'text_delta' | 'thinking_delta' | 'done'}` events; same dual-mode transport, fallback only on establishment failure (never mid-stream, which would duplicate consumed deltas).
44
+ - **`client.responses.createText()`** — convenience returning just the output text.
45
+ - Native-mode mapping extracts joined `output_text` blocks (including refusals), reasoning traces (`thinking`), and parsed `function_call` items (`tool_calls` with parsed `arguments` objects).
46
+ - 404-only fallback, deliberately: 5xx remains a retryable error for the existing retry policy; 4xx is a request bug — neither means "endpoint absent."
47
+ - The full wire-shaped surface stays on `client.openai.responses()`; this module is the ergonomic migration subset documented in the new **MIGRATION.md** (openai npm SDK → this SDK mapping table).
48
+
49
+ ### KV-cache-aware conversation sessions (`src/conversation.ts`)
50
+
51
+ - **`client.session(model, systemPrompt?)`** (or `new ConversationSession(client, options)`) — multi-turn session with an **append-only frozen history** and a system prompt **pinned at construction** (no mutation API — a per-call system prompt is the #1 silent KV-cache killer), maximizing Ollama's prompt-prefix KV-cache reuse across turns.
52
+ - **Per-turn and cumulative cache statistics** from `prompt_eval_cached_count` / `prompt_eval_count`: `session.cacheStats` (cumulative) and `sendTurn()`'s `TurnCacheStats` (per turn).
53
+ - Hit-rate denominator is `cached / (cached + evaluated)`: when the entire prompt hits the cache, Ollama reports `prompt_eval_count: 0`, so dividing by `prompt_eval_count` alone would score the best case as 0%.
54
+ - `send()` (reply text), `sendTurn()` (full turn record), `getMessages()` (defensive copy of the frozen history), `reset()` (restores system prompt, zeroes stats). Per-turn overrides (`options`, `think`, `keep_alive`, `tools`, `format`, `signal`, `timeoutMs`) ride on the request, never the history.
55
+
56
+ ### Context-window safety (`src/context-safety.ts`)
57
+
58
+ - **Pre-flight estimation on every `chat()`/`generate()` call** guards against Ollama's silent prompt truncation when `num_ctx` is unset (server default 2048–4096, model-dependent): CJK-aware heuristic (~1 token per CJK character, ~4 chars/token elsewhere, `IMAGE_TOKEN_ESTIMATE` per image, per-message overhead, tool schemas), ±20–30% by design — no tokenizer download.
59
+ - **`defaultContextLength`** client config — injected as `options.num_ctx` on every request that omits it, making the effective window explicit on the wire.
60
+ - **`onContextOverflow: 'warn' | 'throw'`** (default `'warn'`) — log through the configured logger and send anyway, or reject client-side with `OllamaClientError` (`code: 'context_overflow'`) before anything hits the wire. `contextWarningThreshold` (default `0.9`) tunes the trip point.
61
+ - Window resolution order: request `num_ctx` → client `defaultContextLength` → conservative 2048 fallback, which warns only when the estimate _outright exceeds_ it (no margin — the real server default may be 4096, and a margin would generate false positives).
62
+ - New exports: `estimateTokens()`, `estimateChatRequestTokens()`, `estimateGenerateRequestTokens()`, `checkChatContext()`, `checkGenerateContext()`, `contextWarningMessage()`, `DEFAULT_CONTEXT_WARNING_THRESHOLD`, `IMAGE_TOKEN_ESTIMATE`, `OLLAMA_FALLBACK_CONTEXT_LENGTH`.
63
+
64
+ ### Blob management & custom GGUF model publishing (`src/models-client.ts`)
65
+
66
+ - **`computeBlobDigest(data)`** — SHA-256 via the Web Crypto API (`crypto.subtle`, global in Node ≥ 20 and every browser/edge runtime), formatted `sha256:<64 lowercase hex>` exactly as Ollama's content-addressed store expects.
67
+ - **`createBlobFromData(data, {digest?})`** / **`createBlobFromFile(path)`** — HEAD existence check then upload, skipping the POST (and the wasted bandwidth) when the blob is already present; returns `BlobUploadResult { digest, alreadyExisted }` (file variant adds `fileName` — the basename `/api/create`'s `files` map keys on).
68
+ - **`createModelFromGguf(model, ggufPath | paths, opts)`** — one-shot implementation of Ollama's documented import protocol: push a blob per GGUF shard, then `POST /api/create` with `files: { <fileName>: <digest> }` (verified against the official API reference).
69
+
70
+ ### Docs recheck: Ollama Cloud account endpoints + contract refresh
71
+
72
+ A second full audit against the current official Ollama docs (`docs/openapi.yaml`, `docs/api/*.mdx`, `docs/capabilities/*.mdx`, `docs/modelfile.mdx`, `docs/import.mdx`, `docs/context-length.mdx`) found the SDK already at parity everywhere except three items — all closed here:
73
+
74
+ - **`client.usage(options?)`** — wraps `GET https://ollama.com/api/usage` (cloud-only): request counts, USD spend, and token totals (input / cached-input / output) across the whole range plus hourly (`range: '24h'`) or daily (`'7d'`/`'30d'`) buckets, including `partial: true` markers for the in-progress hour/day. Query parameters `range` (`'24h' | '7d' | '30d'`, server default `'7d'`) and `scope` (`'self' | 'team'`, server default `'self'`, team requires an admin) are typed and only sent when provided.
75
+ - **`client.balance(options?)`** — wraps `GET https://ollama.com/api/balance`: remaining included + purchased credits, with the included shape a runtime-discriminated union of the plan-period credits object (`balance_usd`/`allowance_usd`/`period`) and the legacy session/weekly percentage-limits shape.
76
+ - Both ride the same fixed-cloud-host pipeline as `webSearch`/`webFetch` (`https://ollama.com`, `apiKey`/`OLLAMA_API_KEY`, default timeout + retry, never multi-endpoint failover) and both endpoints' documented 10-req/min rate limit is respected by the shared `OllamaRateLimitError` retry semantics (429 + `Retry-After`).
77
+ - **`CreateRequestOptions.capabilities`** — the one field-level gap the drift detector surfaced: the official `/api/create` payload accepts `capabilities: string[]` (e.g. `'decision'`) to add capabilities without removing inherited/inferred ones; now typed and forwarded. `files` also joined the create parity manifest.
78
+ - **Contract refresh** — the pinned OpenAPI snapshot (`contracts/sources/ollama.openapi.yaml` + verbatim `docs/upstream/ollama-openapi.yaml`) moved from the 2026-10-02 pin to 2026-10-07 (source SHA `675c8bb`), bringing the `Usage*`/`Balance*` schemas into the IR (7 new contract-generated models), repairing the corrupted enum literals the upstream file carries (`[24h, 7d, 30d]`, `[hour, day]`), and adding a new `contracts/overlays/cloud.yaml` so bidirectional endpoint discovery, the MCP tool registry, and the IR inspector all see the account endpoints.
79
+ - **Generator fix**: top-level `allOf` schemas (like `UsageBucket` = `UsageMetrics & {from, until, partial?}`) previously emitted as an empty interface — the models emitter dropped the intersection. `allOf` now emits a correct type alias; the Zod emitter already handled it.
80
+ - Everything else audited clean: all 15 core REST endpoints (incl. legacy `/api/embeddings`), `/v1/systemone`, `web_search`/`web_fetch`, the full OpenAI-compat surface (incl. `reasoning_effort`, `reasoning.effort`, `think`, `instructions`, `max_output_tokens`) and Anthropic-compat surface (incl. `output_config`, thinking, tool results, all streaming events), field-level sync on every request/response schema (`logprobs`, `top_logprobs`, `think`, `remote_host`/`remote_model`, `draft_files`, `parser`, `renderer`, `requires`, Ps `context_length`/`size_vram`, Show `capabilities`/`thinking`), auth (`OLLAMA_API_KEY` scoping), and the capability docs (vision, structured outputs, thinking, embeddings, tool calling, context length, GGUF import).
81
+ - New exports: `client.usage()`, `client.balance()`, `UsageRequestOptions`, `UsageRange`, `UsageScope`, `BalanceRequestOptions`, and the contract-generated `UsageResponse`, `UsageMetrics`, `UsageBucket`, `BalanceResponse`, `IncludedBalance`, `LegacyBalanceLimit`, `LegacyIncludedBalance`.
82
+ - 10 new unit tests (`test/usage-balance.test.ts`, mocked fetch — CI-safe) and updated guard tests (`test/security-invariants.test.ts` host allowlist now covers the four cloud ops; `test/contract/parity.test.ts` new-ops set extended).
83
+
84
+ ### Documentation & examples
85
+
86
+ - README: four new Quick Start sections (sessions, dual-mode Responses, context safety, blob publishing) + rewritten vision section; five new feature bullets.
87
+ - **MIGRATION.md** (new): openai npm SDK → this SDK mapping table, responses/chat-completions/vision/statefulness/reasoning/error mapping.
88
+ - **ADR 0024** (new) + ADR index now lists 0022–0024.
89
+ - 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.
90
+ - 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`.
91
+
92
+ ## [Unreleased]
93
+
94
+ ### Fix: IPv6 loopback was classified as Ollama Cloud
95
+
96
+ - `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`.
97
+
98
+ ### Host normalization and scoped `OLLAMA_API_KEY`
99
+
100
+ - **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.
101
+ - **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.
102
+
103
+ ### Production-readiness audit — tool-call accumulator, mock server
104
+
105
+ - **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:
106
+ - `GET /api/version`, `/api/tags`, `/api/ps`, `POST /api/show` schema conformance
107
+ - `POST /api/chat`, `/api/generate`, `/api/embed` schema conformance (non-streaming)
108
+ - `POST /api/chat`, `/api/generate` streaming chunk schema conformance
109
+ - Error-class mapping: 404 → `OllamaNotFoundError`, 503 → `OllamaOverloadedError`, in-band `{"error":"..."}` → `OllamaStreamError`
110
+ - Optional telemetry fields (`prompt_eval_cached_count`, `thinking`, `done_reason`) — verifies the SDK does NOT throw when these are absent (the Wave 13 conformance failure mode)
111
+ - `done_reason` variants: `"stop"`, `"length"`, `"load"`, `"unload"` all parse cleanly; an undocumented value also parses cleanly (forward-compat)
112
+ - `OllamaClient` vs `NativeApi` shape parity (mirrors the live parity test)
113
+ - Updated `test/conformance/harness.ts` docstring to document the live-vs-offline split.
114
+
115
+ ### Production-readiness audit — tool-call accumulator, mock server
116
+
117
+ - **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.
118
+ - New exports: `ToolCallAccumulator` class, `mergeToolCallArrays()` stateless helper, `mergeToolCall()` / `mergeToolCallArgumentsString()` / `isSameToolCall()` standalone helpers.
119
+ - `aggregateChat` now uses `mergeToolCallArrays` instead of the naive `[...accumulated, ...incoming]` spread.
120
+ - Includes a dedicated `mergeToolCallArgumentsString` helper for the OpenAI-compat streaming format (where `function.arguments` arrives as string deltas like `'{"city":'` + `' "Bengaluru"}'`); the OpenAI-compat bridge at `src/integrations/openai.ts` already does its own per-index accumulation, but the helper is exported for callers who want the same logic elsewhere.
121
+
122
+ - **In-memory mock server for deterministic CI testing (`test/mocks/ollama-mock-server.ts`).** A thin `node:http` wrapper that lets tests register route handlers with programmable delay, chunk fragmentation, mid-stream error injection, and connection drops. Complements the existing VCR cassette system (`test/vcr.ts`) with capabilities cassettes can't provide:
123
+ - Programmable inter-chunk delay (test backpressure handling).
124
+ - Mid-stream error frame injection (test in-band `{"error":"..."}` trapping).
125
+ - Chunk fragmentation across TCP packets (test parser buffering).
126
+ - 503/502/429 error status codes with custom bodies.
127
+ - Connection drops mid-stream (test recovery from network failures).
128
+ - Request body capture via `onRequest` callback.
129
+ - Idempotent `stop()` — safe to call from `afterEach` hooks without tracking whether `start()` ran.
130
+ - Ephemeral port support (`port: 0`) for parallel test runs.
131
+
132
+ ### Second digest — mid-stream errors, 502/503 specialization, pull-progress ergonomics
133
+
134
+ - **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.
135
+ - `parseNdjsonStream` now detects bare `{"error": "..."}` frames and throws `OllamaStreamError` immediately, with the error message preserved.
136
+ - `OllamaStreamError.partialContent` carries whatever content was accumulated before the error frame arrived (concatenated `message.content` for chat streams, concatenated `response` for generate streams, empty string for pull/push streams).
137
+ - The stream's `finalResult` promise rejects with the enriched `OllamaStreamError`; the `for await ... of stream` iterator yields an `{ type: 'error', data: { error } }` event before completing.
138
+ - See: https://github.com/ollama/ollama/blob/main/docs/api.md
139
+
140
+ - **Granular HTTP status specialization.** Two new error subclasses give callers a way to branch on the failure mode rather than parsing messages or branching on `status` numbers.
141
+ - `OllamaOverloadedError` (HTTP 503) — server queue saturation (`OLLAMA_MAX_QUEUE` exceeded, default 512). `retryable: true`.
142
+ - `OllamaBadGatewayError` (HTTP 502) — cloud model could not be reached by the Ollama Cloud proxy. `retryable: true`.
143
+ - Both extend `OllamaClientError` directly (NOT `OllamaServerError`) because `OllamaServerError`'s constructor unconditionally forces `code: 'server_error'`. Callers wanting "any 5xx" should branch on `error.status >= 500 && error.status < 600` rather than `instanceof OllamaServerError`.
144
+ - Existing `instanceof` handlers for `OllamaNotFoundError` (404), `OllamaRateLimitError` (429), and `OllamaServerError` (generic 5xx) remain unchanged.
145
+
146
+ - **Typed pull/push progress callback (`onProgress`).** The SDK already exposed pull/push progress as a typed `OllamaStream` via `models.pull({ stream: true })`. The new `onProgress(stream, cb)` helper wraps the stream iteration with a pre-computed `percent` field for callers building download/upload UIs:
147
+
148
+ ```ts
149
+ const stream = await client.models.pull({ model: 'llama3.2', stream: true });
150
+ await onProgress(stream, (event) => {
151
+ if (event.percent !== undefined) {
152
+ console.log(`${event.status}: ${event.percent}% (${event.digest?.slice(0, 12) ?? '-'})`);
153
+ }
154
+ });
155
+ ```
156
+ - `computeProgressPercent(chunk)` and `toPullProgressEvent(chunk)` exported for callers who want the percent calculation without subscribing to a stream.
157
+ - `PullProgressEvent` type carries `{ status, digest?, total?, completed?, percent?, raw }`.
158
+
159
+ - **Cloud-vs-local tool-replay investigation (ADR 0023).** Documented the SDK's stance on parallel-vs-sequential tool-call emission from cloud vs local models: the SDK is a transparent passthrough, models decide, agent authors enforce sequential execution via `ToolRegistry`'s `maxConcurrency` option if they need it. Implemented the proposed `ModelCapabilities.parallelToolCalls` field as a best-effort heuristic:
160
+ - New `ParallelToolCallBehavior` union: `'yes' | 'no' | 'unknown'`.
161
+ - Cloud-mode + tool-capable → `'yes'` (the OpenAI/Anthropic compat bridges proxy to proprietary models that freely emit parallel tool calls).
162
+ - Cloud-mode + no tools → `'unknown'`.
163
+ - Local-mode + family in known-parallel list (`qwen2`, `qwen2.5`, `qwen3`, `llama3.1`, `llama3.2`, `llama3.3`, `llama4`, `mistral`, `mixtral`, `hermes`, `command-r`, `command-r-plus`) → `'yes'`.
164
+ - Local-mode + model name (including `:tag`) contains a tool-use variant hint (`tool-use`, `instruct`, `hermes`, `command-r`, etc.) → `'yes'`.
165
+ - Otherwise → `'unknown'` (caller should treat as "ask the model and see what it does").
166
+ - The family is extracted from `/api/show`'s `model_info.*.architecture` (with fallbacks to `details.family` then `details.families[0]`).
167
+
168
+ ### First digest — VRAM lifecycle, fetchWithBackoff, System One image caps, done_reason decoder
169
+
170
+ - **VRAM lifecycle primitives.** Added ergonomic helpers for explicit GPU memory management, addressing the upstream `keep_alive` semantics documented in the Ollama FAQ:
171
+ - `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.
172
+ - `ModelsClient.unload(model)` issues an empty `/api/generate` request with `keep_alive: 0` to evict a model from VRAM immediately, freeing GPU memory for subsequent pipelines.
173
+ - `ModelsClient.pin(model)` issues an empty `/api/generate` request with `keep_alive: -1` to pre-load and pin a model indefinitely for hot-loop inference.
174
+ - `OllamaClient.unloadModel(model)` and `OllamaClient.pinModel(model)` convenience aliases mirror the existing `listModels` / `pullModel` pattern.
175
+ - `KEEP_ALIVE_UNLOAD` and `KEEP_ALIVE_INDEFINITE` exported constants for callers who want the raw sentinel values directly.
176
+
177
+ - **Overload resiliency for HTTP 503 / 429.** New standalone `fetchWithBackoff()` helper wraps any fetch-shaped function with jittered exponential backoff for transient saturation responses. Mirrors the SDK's internal transport retry policy (`maxRetries: 3`, `initialDelayMs: 500`, `maxDelayMs: 30_000`, full-jitter strategy) so consumers see identical retry behavior whether they go through `OllamaClient` or call the helper directly. Useful for raw HTTP traffic to Ollama's compatibility bridges (Anthropic / OpenAI) or sibling Ollama instances outside the configured endpoint registry. `RETRYABLE_STATUS_CODES` and `DEFAULT_FETCH_BACKOFF_CONFIG` exported for downstream tooling.
178
+
179
+ - **System One image support ergonomics.** The generated `SystemOneRequest.images?: readonly string[]` field (already on the wire) is now backed by:
180
+ - `MAX_SYSTEM_ONE_REQUEST_BYTES` (64 KiB) and `MAX_SYSTEM_ONE_IMAGES_BYTES` (32 MiB) constants documenting the conditional server-side size limits.
181
+ - `estimateSystemOneRequestBytes(request)` helper for pre-flight size validation before sending a multi-image batch — the runtime enforces the limit too, but the early check avoids the round-trip when the caller already knows the payload is too large.
182
+
183
+ - **Response decoder hardening (Wave 13 conformance fix).** Added `KnownDoneReason` literal type (`'stop' | 'length' | 'load' | 'unload'`) and `isKnownDoneReason()` type guard so callers can narrow `done_reason` from `string | undefined` without risking `TypeError` on undefined access — the failure mode flagged in CI run #36962280416. The `done_reason` field on `ChatResponse` / `GenerateResponse` remains `string | undefined` for forward-compat with future upstream additions; the type guard provides opt-in narrowing.
184
+
3
185
  ## [1.8.0] - 2026-10-02
4
186
 
5
187
  - **Contract execution completion (Waves 12-16).** The SDK's contract-first architecture is now fully executable: every operation in the canonical IR is represented, generated, typed, and runtime-enforced with no `Record<string, unknown>` escape hatches on the generated surface.
package/README.md CHANGED
@@ -12,19 +12,25 @@
12
12
 
13
13
  - 🚀 **Native Web Standards**: Built on native `fetch` and Web Streams. Zero external HTTP dependencies.
14
14
  - 🧠 **Reasoning & Thinking Tokens**: First-class support for reasoning models (`qwen3:8b`, `deepseek-r1:8b`) via the native `think` parameter, with discrete `thinking` and `token` streaming events, plus `logprobs`/`top_logprobs` for token-level confidence scoring.
15
- - 🖼️ **Multimodal / Vision Input**: `images` on `Message`/`generate()` accepts base64 strings or raw `Uint8Array` bytes (auto-encoded) for vision models like `llava` or `qwen2.5vl`.
15
+ - 🖼️ **Multimodal / Vision Input**: `images` on `Message`/`generate()` is a universal resolver — pass raw base64 strings, `data:image/...;base64,` data URIs, `http(s)://` URLs (fetched), local file paths (`.png`/`.jpg`/`.webp`/…, read via `node:fs` on Node), or raw `Buffer`/`Uint8Array` bytes — everything is normalized to the raw base64 Ollama's REST API expects, matching the official SDK vision convention.
16
16
  - 🎯 **Zod-Powered Structured Outputs**: Strictly typed schema enforcement via `chatWithSchema` and `generateWithSchema` with resilient markdown JSON parsing.
17
17
  - 🛠️ **Autonomous Agent & Tool Calling**: Multi-turn agent loop (`Agent`) with automated tool execution, parameter validation, and self-correcting error recovery.
18
18
  - 🌐 **High Availability & Failover**: Multi-endpoint registry with priority routing, circuit breaker failover, active health checks, and per-endpoint `models` allow-lists for routing several model-specific API keys through one client.
19
19
  - 🔌 **Model Context Protocol (MCP)**: First-class, transport-neutral `McpBridge` for converting MCP tool descriptors into Ollama function definitions and registering executable MCP-backed tools.
20
20
  - 📚 **Full Model Lifecycle**: `pullModel`, `pushModel`, `createModel`, `copyModel`, `deleteModel`, `listModels`, `showModel`, and `ps()` (currently loaded models) — full parity with Ollama's model management API.
21
21
  - 🔎 **Ollama Cloud Web Tools**: `webSearch`/`webFetch` wrap Ollama's hosted `/api/web_search` and `/api/web_fetch` tools at `ollama.com` (requires an `OLLAMA_API_KEY`), independent of any local `baseUrl`.
22
- - 🌉 **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.
22
+ - 💳 **Ollama Cloud Usage & Balance**: `usage()`/`balance()` wrap Ollama's hosted account endpoints (`GET /api/usage`, `GET /api/balance`) — request counts, USD spend, cached-token totals bucketed by hour/day, and remaining included/purchased credits (including the legacy session/weekly plan shapes) — with the same fixed-cloud-host, API-key, timeout, and retry semantics as the web tools.
23
+ - 🌉 **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).
24
+ - 💬 **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, and surfaces per-turn + cumulative cache hit rates (`prompt_eval_cached_count`) so cache degradation is visible instead of silent.
25
+ - 🛑 **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.
26
+ - 📦 **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}`).
27
+ - 🧵 **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.
28
+ - 🧹 **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.
23
29
  - 🌊 **Web Stream Adapters**: Drop-in adapters (`toTextStream`, `toDataStream`, `toResponse`) for Next.js Route Handlers and Vercel AI SDK.
24
30
  - 📈 **OpenTelemetry Instrumentation**: Automatic spans for HTTP requests, endpoint failover, chat/generate calls, and agent runs — zero-cost when OpenTelemetry isn't installed.
25
31
  - 📊 **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.
26
32
  - ⚡ **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.
27
- - 📦 **Dual ESM & CJS Build**: Full module support with clean TypeScript `.d.ts` declaration maps.
33
+ - 📦 **Dual ESM & CJS Build**: Full module support with paired `.d.ts`/`.d.cts` declarations mapped through conditional `exports` — verified in CI by `@arethetypeswrong/cli` **and** by compiling real CommonJS (node16) and ESM (nodenext) consumer projects against the packed tarball.
28
34
  - 🧩 **Contract-First Architecture**: A single canonical IR (`contracts/ir/ollama.ir.json`) drives TypeScript interfaces, generated API classes (`NativeApi`/`OpenAIApi`/`AnthropicApi`), MCP tool descriptors, Zod schemas, and field-level parity verification. New Ollama endpoints (like `/v1/systemone`) are caught automatically by bidirectional endpoint discovery. See [ADRs 0013-0019](./docs/adr/README.md).
29
35
 
30
36
  ---
@@ -241,6 +247,105 @@ console.log(res.prompt_eval_count, res.prompt_eval_cached_count);
241
247
  console.log(res.prompt_eval_cached_count ?? 0);
242
248
  ```
243
249
 
250
+ ### KV-Cache-Aware Conversation Sessions
251
+
252
+ Ollama reuses the model's KV cache across turns by **prompt prefix matching** — anything that mutates the prefix (changing the system prompt, injecting per-call timestamps, reordering past turns) invalidates the whole cache and forces a full re-evaluation. `client.session()` makes the cache-friendly structure the default: a system prompt pinned at construction, an append-only frozen history, and per-turn options that never touch the prefix.
253
+
254
+ ```typescript
255
+ const session = client.session('llama3.1', 'You are a concise assistant.');
256
+
257
+ // Ergonomic path — just the reply text
258
+ await session.send('Hi!');
259
+
260
+ // Detailed path — per-turn KV-cache statistics + raw response
261
+ const turn = await session.sendTurn('Why is the sky blue?');
262
+ console.log(turn.cache);
263
+ // { cachedTokens: 48, evaluatedTokens: 6, hitRate: 0.89 }
264
+
265
+ // Cumulative view across the whole session
266
+ console.log(session.cacheStats);
267
+ // { turns: 2, cachedTokens: 48, evaluatedTokens: 54, totalPromptTokens: 102, hitRate: 0.47 }
268
+
269
+ session.reset(); // back to system-prompt-only, stats zeroed
270
+ session.getMessages(); // defensive copy of the frozen history
271
+ ```
272
+
273
+ Note the hit-rate denominator: when the _entire_ prompt hits the cache, Ollama reports `prompt_eval_count: 0` — the session's `hitRate` adds both counters so the best case scores `1`, not `0`.
274
+
275
+ ### OpenAI Responses API (dual-mode)
276
+
277
+ `client.responses.create()` gives migrating OpenAI code a small, familiar surface (`model`, `input`, `instructions`, `tools`, `temperature`, `top_p`, `max_output_tokens`, `reasoning_effort`, `think`) with **dual-mode transport**: the native `POST /v1/responses` endpoint is preferred (Ollama ≥ v0.13.3), and a `404` from older servers transparently re-issues the request through `/api/chat` — `instructions` mapped to a system message, the reply mapped back into the Responses shape. The result's `transport` field records which path served it.
278
+
279
+ ```typescript
280
+ const res = await client.responses.create({
281
+ model: 'gpt-oss:20b',
282
+ input: 'Explain KV caches in one paragraph.',
283
+ instructions: 'Be precise.',
284
+ max_output_tokens: 256,
285
+ reasoning_effort: 'medium',
286
+ });
287
+
288
+ console.log(res.output_text);
289
+ console.log(res.usage); // { input_tokens, output_tokens, total_tokens }
290
+ console.log(res.transport); // 'native' | 'chat-adapter'
291
+
292
+ // Streaming variant — simplified text/thinking deltas + final response
293
+ for await (const event of client.responses.stream({ model: 'qwen3', input: 'Tell me a haiku.' })) {
294
+ if (event.type === 'text_delta') process.stdout.write(event.delta);
295
+ else if (event.type === 'done') console.log('\n', event.response.usage);
296
+ }
297
+ ```
298
+
299
+ Ollama implements the Responses API **non-statefully** — `previous_response_id` and `conversation` don't exist, so send the full conversation in `input` every call (or use `ConversationSession` to manage it). For the full OpenAI-shaped surface (raw `output` items, SSE event objects), use `client.openai.responses()`.
300
+
301
+ ### Context-Window Safety (silent-truncation guard)
302
+
303
+ With `num_ctx` unset, Ollama loads the model with a conservative default window (commonly 2048–4096 tokens) and **silently truncates** prompts that don't fit — the model simply loses the top of the conversation with no error. The client estimates the prompt size before every `chat`/`generate` call and surfaces the risk:
304
+
305
+ ```typescript
306
+ const client = new OllamaClient({
307
+ // Make the window explicit on every request that omits options.num_ctx
308
+ defaultContextLength: 8192,
309
+
310
+ // 'warn' (default): log through `logger` and send anyway
311
+ // 'throw': reject client-side with OllamaClientError before anything hits the wire
312
+ onContextOverflow: 'warn',
313
+ });
314
+
315
+ // Warnings fire when the estimate crosses 90% of the effective window:
316
+ // "OllamaClient.chat(): estimated prompt size ~1900 tokens is close to or
317
+ // beyond the request's context window (num_ctx=2048). The server will
318
+ // likely silently truncate …"
319
+ ```
320
+
321
+ 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`).
322
+
323
+ ### Blob Management & Custom GGUF Model Publishing
324
+
325
+ Ollama's documented import protocol: push a blob for each file, then reference it in `/api/create`'s `files` map by file name and SHA-256 digest. The SDK wraps the whole flow:
326
+
327
+ ```typescript
328
+ // Content-addressed digest: SHA-256 -> "sha256:<64 lowercase hex>"
329
+ const digest = await client.models.computeBlobDigest(ggufBytes);
330
+
331
+ // HEAD-check + upload (skips the POST when the blob already exists)
332
+ const { digest, alreadyExisted } = await client.models.createBlobFromData(ggufBytes);
333
+ const upload = await client.models.createBlobFromFile('/models/my-model.gguf');
334
+ // -> { digest: 'sha256:…', alreadyExisted: false, fileName: 'my-model.gguf' }
335
+
336
+ // One-shot: upload every GGUF shard, then POST /api/create with files: {name: digest}
337
+ await client.models.createModelFromGguf('my-model', '/models/my-model.gguf', {
338
+ template: '{{ .Prompt }}',
339
+ parameters: { temperature: 0.7 },
340
+ });
341
+
342
+ // Split GGUFs: pass every shard
343
+ await client.models.createModelFromGguf('my-model', [
344
+ '/models/model-00001-of-00002.gguf',
345
+ '/models/model-00002-of-00002.gguf',
346
+ ]);
347
+ ```
348
+
244
349
  ### Token Log Probabilities (`logprobs`)
245
350
 
246
351
  Set `logprobs: true` (optionally with `top_logprobs`) on `chat()`/`generate()` to get per-token log probabilities back — useful for confidence scoring, speculative decoding, or agent routing decisions.
@@ -261,27 +366,62 @@ for (const entry of res.logprobs ?? []) {
261
366
 
262
367
  ### Multimodal / Vision Input
263
368
 
264
- `images` on a `Message` (or on `generate()`'s top-level request) accepts base64-encoded strings or raw `Uint8Array` bytes — `Uint8Array` entries are base64-encoded automatically before the request is sent.
369
+ `images` on a `Message` (or on `generate()`'s top-level request) is a **universal vision resolver** — every polymorphic form is normalized to the raw base64 string Ollama's REST API expects (no `data:image/...;base64,` URI prefixes ever reach the wire):
265
370
 
266
371
  ```typescript
267
- import { readFile } from 'node:fs/promises';
372
+ // 1. Local file paths (Node.js — read via dynamically-imported node:fs)
373
+ const res = await client.chatText({
374
+ model: 'llava',
375
+ messages: [
376
+ {
377
+ role: 'user',
378
+ content: 'What is in this image?',
379
+ images: ['./cat.png'],
380
+ },
381
+ ],
382
+ });
268
383
 
269
- const imageBytes = await readFile('./cat.png'); // Buffer, a Uint8Array subclass
384
+ // 2. Data URIs — the header is stripped automatically
385
+ await client.chat({
386
+ model: 'llava',
387
+ messages: [
388
+ {
389
+ role: 'user',
390
+ content: 'Describe this.',
391
+ images: ['data:image/png;base64,iVBORw0KGgo...'],
392
+ },
393
+ ],
394
+ });
270
395
 
271
- const res = await client.chatText({
396
+ // 3. Web URLs — fetched and encoded
397
+ await client.chat({
398
+ model: 'llava',
399
+ messages: [{ role: 'user', content: 'OCR this.', images: ['https://example.com/receipt.png'] }],
400
+ });
401
+
402
+ // 4. Raw bytes — Buffer / Uint8Array, base64-encoded automatically
403
+ import { readFile } from 'node:fs/promises';
404
+ await client.chat({
272
405
  model: 'llava',
273
- messages: [{ role: 'user', content: 'What is in this image?', images: [imageBytes] }],
406
+ messages: [
407
+ {
408
+ role: 'user',
409
+ content: 'What is in this image?',
410
+ images: [await readFile('./cat.png')],
411
+ },
412
+ ],
274
413
  });
275
- console.log(res);
276
414
 
277
- // Base64 strings work too, unchanged:
278
- const base64Res = await client.generateText({
415
+ // 5. Plain base64 strings — passed through unchanged
416
+ await client.generateText({
279
417
  model: 'llava',
280
418
  prompt: 'Describe this image.',
281
419
  images: ['iVBORw0KGgoAAAANSUhEUgAA...'],
282
420
  });
283
421
  ```
284
422
 
423
+ The resolver is also exported standalone for custom pipelines: `resolveImageInput(input)` (single) and `resolveImages(images)` (batch, parallel). In browsers/edge runtimes, file-path strings throw a descriptive error (no `node:fs`) — read the file yourself and pass the bytes.
424
+
285
425
  ### Structured Outputs with Zod
286
426
 
287
427
  ```typescript
@@ -324,6 +464,31 @@ console.log(
324
464
 
325
465
  `embed()` targets the modern `/api/embed` endpoint (batch `input`, `truncate`, and `dimensions` truncation are all supported). The older single-prompt `/api/embeddings` is still available as `client.embeddings()`, but it's `@deprecated` — Ollama's own docs consider it legacy in favor of `/api/embed`.
326
466
 
467
+ #### Batched embeddings with backpressure (large corpora)
468
+
469
+ Embedding a whole corpus with `Promise.all(chunks.map(c => client.embed(...)))` is the failure mode every RAG pipeline eventually hits: hundreds of simultaneous requests contend sockets, saturate the daemon's request queue (`OLLAMA_MAX_QUEUE` → 503s), and spike GPU VRAM as the model instance serves every prompt at once. Long inputs have a second, quieter failure mode — Ollama **silently truncates** any input string longer than the model's context window unless `truncate: false` is set.
470
+
471
+ `embedBatch()` is the safe building block for ingestion:
472
+
473
+ ```typescript
474
+ const { embeddings, batchCount } = await client.embedBatch({
475
+ model: 'nomic-embed-text:latest',
476
+ input: corpus, // any size — 10 strings or 10 million
477
+ batchSize: 32, // inputs per /api/embed request (default 32)
478
+ concurrency: 3, // batches in flight simultaneously (default 3)
479
+ keep_alive: '10m', // pin the model for the whole ingestion
480
+ onBatchComplete: (done, total) => progress.log(`${done}/${total} batches`),
481
+ });
482
+
483
+ // embeddings[i] always corresponds to corpus[i], no matter which batch
484
+ // finished first — safe to zip straight into a vector store.
485
+ await vectorStore.upsert(corpus.map((text, i) => ({ id: i, text, vector: embeddings[i] })));
486
+ ```
487
+
488
+ Every batch rides the client's full pipeline — failover, retry, auth, telemetry — and the operation is **order-preserving** and **fail-fast**: the first batch error aborts its in-flight siblings and rejects the whole call with the original error, so a half-indexed corpus never looks like success. The caller's `signal` cancels queued and in-flight batches alike (`code: 'aborted'`).
489
+
490
+ Like `chat`/`generate`, `embedBatch()` injects `defaultContextLength` as `num_ctx` and pre-checks each input string's estimated tokens against the resolved window — warn (default) or throw (`onContextOverflow: 'throw'`) with the offending indexes _before_ any request is sent, since Ollama truncates embedding inputs silently. Tune `batchSize`/`concurrency` per daemon: lower both for a laptop GPU, raise `concurrency` for remote or multi-endpoint clients.
491
+
327
492
  ### Model Lifecycle Management
328
493
 
329
494
  Full parity with Ollama's model catalog and blob-store API — every method targets one specific endpoint's local state and deliberately does **not** cross-endpoint fail over (see [ADR 0008](./docs/adr/0008-endpoint-failover-scope.md)):
@@ -367,6 +532,39 @@ console.log(page.title, page.content.slice(0, 200));
367
532
 
368
533
  These two methods don't participate in the multi-endpoint failover below — there's only ever the one cloud host to call — but they do use the same default `timeoutMs` and retry policy as everything else.
369
534
 
535
+ ### Cloud Usage & Balance (Ollama Cloud account)
536
+
537
+ `usage`/`balance` wrap Ollama's **hosted account endpoints** (`GET https://ollama.com/api/usage` and `/api/balance`) — the same fixed Ollama Cloud service and API-key requirements as `webSearch`/`webFetch` above, so everything said there about auth, timeouts, and retry applies identically here:
538
+
539
+ ```typescript
540
+ const client = new OllamaClient({
541
+ baseUrl: 'http://localhost:11434', // local inference — unrelated to the calls below
542
+ apiKey: process.env.OLLAMA_API_KEY, // required for usage/balance specifically
543
+ });
544
+
545
+ // Usage for the last 24 hours, bucketed hourly (omit options for the server
546
+ // defaults: range='7d', scope='self'). Team scope requires a team admin.
547
+ const usage = await client.usage({ range: '24h' });
548
+ console.log(usage.totals.request_count, usage.totals.usage_usd, usage.totals.cached_input_tokens);
549
+ for (const bucket of usage.buckets) {
550
+ if (bucket.partial) continue; // the current hour — still in progress
551
+ console.log(bucket.from, bucket.request_count);
552
+ }
553
+
554
+ // Remaining credits. `included` is either the plan-period credits object
555
+ // (`balance_usd`/`allowance_usd`/`period`) or, on legacy plans, the
556
+ // session/weekly percentage-limits shape — discriminated by the response.
557
+ const balance = await client.balance();
558
+ if ('balance_usd' in balance.included) {
559
+ console.log(`included: $${balance.included.balance_usd} of $${balance.included.allowance_usd}`);
560
+ } else {
561
+ console.log(`session limit: ${balance.included.session.remaining_percent}% remaining`);
562
+ }
563
+ console.log(`purchased: $${balance.purchased.balance_usd}`);
564
+ ```
565
+
566
+ Both endpoints are rate-limited to 10 requests/minute per user (429 responses carry `Retry-After`); polling about once a minute is the recommended cadence.
567
+
370
568
  ### Autonomous Agent & Tool Calling
371
569
 
372
570
  ```typescript
@@ -791,6 +989,37 @@ initial HTTP round trip:
791
989
  between turns, outside any `chat()` call, so a slow tool never ties up one of your
792
990
  scarce concurrent-request accounts.
793
991
 
992
+ ### Client Teardown: `destroy()`
993
+
994
+ Inside `node:worker_threads` tasks, CLI runners, serverless handlers, and other
995
+ short-lived processes, the things that keep an event loop (and therefore the
996
+ thread or process) alive are exactly the things this SDK manages: in-flight
997
+ fetch bodies, unconsumed stream readers, and requests queued behind
998
+ `maxConcurrentPerEndpoint`. `destroy()` is the one-call drain for all of them:
999
+
1000
+ ```typescript
1001
+ import { parentPort, workerData } from 'node:worker_threads';
1002
+
1003
+ const client = new OllamaClient({ baseUrl: workerData.ollamaUrl });
1004
+ try {
1005
+ await processTask(client, workerData.payload);
1006
+ } finally {
1007
+ // Aborts every request/stream still in flight with OllamaAbortError
1008
+ // (code 'aborted'), releases their endpoint slots, and lets the worker
1009
+ // thread exit cleanly. Idempotent — safe to call from a signal handler.
1010
+ const aborted = client.destroy('worker task finished');
1011
+ if (aborted > 0) logger.warn(`torn down ${aborted} in-flight operations`);
1012
+ }
1013
+ ```
1014
+
1015
+ Every aborted operation — dispatched **or** still queued — rejects with
1016
+ `OllamaAbortError` carrying the reason; active streams reject their
1017
+ `finalResult` and release their concurrency slots, so nothing dangles after
1018
+ the call returns the count of operations it aborted. `destroy()` is a drain,
1019
+ not a permanent disable: the client stays usable afterward (spawn a fresh one
1020
+ if you want a hard cut), and calling it on an idle client is a no-op that
1021
+ returns `0`.
1022
+
794
1023
  ---
795
1024
 
796
1025
  ### Observability with OpenTelemetry
@@ -1185,6 +1414,13 @@ npm run bench
1185
1414
 
1186
1415
  # Run full CI verification pipeline (typecheck, lint, test, build, edge runtime check)
1187
1416
  npm run verify
1417
+
1418
+ # Non-disastrous smoke test: contract gates (validate, IR diff, type drift) + full
1419
+ # verify chain + package type-resolution check + real CJS/ESM consumer compile.
1420
+ # Read-only — no clean, no codegen rewriting src/generated. Live conformance runs
1421
+ # automatically if an Ollama server is reachable on localhost:11434 (otherwise
1422
+ # those tests skip cleanly).
1423
+ npm run smoke
1188
1424
  ```
1189
1425
 
1190
1426
  ---
@@ -1,4 +1,4 @@
1
- import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, ab as SseEvent, w as OllamaClientError } from './runtime-C6UYtBRB.js';
1
+ import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, ae as SseEvent, x as OllamaClientError } from './runtime-BV9SK_cf.js';
2
2
 
3
3
  /**
4
4
  * AUTO-GENERATED by `npm run contract:generate`. DO NOT EDIT.
@@ -139,6 +139,8 @@ interface CreateRequest {
139
139
  readonly draft_quantize?: string | undefined;
140
140
  /** Minimum Ollama version required by the model */
141
141
  readonly requires?: string | undefined;
142
+ /** Capabilities to add without removing inherited or inferred capabilities (e.g. `decision` for compatible decision models) */
143
+ readonly capabilities?: readonly string[] | undefined;
142
144
  /** Stream status updates */
143
145
  readonly stream?: boolean | undefined;
144
146
  }
@@ -342,7 +344,7 @@ interface ModelSummary {
342
344
  /** Primary model family (for example `llama`) */
343
345
  readonly family?: string | undefined;
344
346
  /** All families the model belongs to, when applicable */
345
- readonly families?: readonly string[] | undefined;
347
+ readonly families?: readonly string[] | null | undefined;
346
348
  /** Approximate parameter count label (for example `7B`, `13B`) */
347
349
  readonly parameter_size?: string | undefined;
348
350
  /** Quantization level used (for example `Q4_0`) */
@@ -1,4 +1,4 @@
1
- import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, ab as SseEvent, w as OllamaClientError } from './runtime-DTmUeU6K.cjs';
1
+ import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, ae as SseEvent, x as OllamaClientError } from './runtime-BsdBbuOv.cjs';
2
2
 
3
3
  /**
4
4
  * AUTO-GENERATED by `npm run contract:generate`. DO NOT EDIT.
@@ -139,6 +139,8 @@ interface CreateRequest {
139
139
  readonly draft_quantize?: string | undefined;
140
140
  /** Minimum Ollama version required by the model */
141
141
  readonly requires?: string | undefined;
142
+ /** Capabilities to add without removing inherited or inferred capabilities (e.g. `decision` for compatible decision models) */
143
+ readonly capabilities?: readonly string[] | undefined;
142
144
  /** Stream status updates */
143
145
  readonly stream?: boolean | undefined;
144
146
  }
@@ -342,7 +344,7 @@ interface ModelSummary {
342
344
  /** Primary model family (for example `llama`) */
343
345
  readonly family?: string | undefined;
344
346
  /** All families the model belongs to, when applicable */
345
- readonly families?: readonly string[] | undefined;
347
+ readonly families?: readonly string[] | null | undefined;
346
348
  /** Approximate parameter count label (for example `7B`, `13B`) */
347
349
  readonly parameter_size?: string | undefined;
348
350
  /** Quantization level used (for example `Q4_0`) */
@@ -1,4 +1,4 @@
1
- import { p as ToolDefinition, q as ToolCall, r as Message } from './types-5PAjLoCp.js';
1
+ import { l as ToolDefinition, i as ToolCall, n as Message } from './types-BtQexFpT.js';
2
2
  import { z } from 'zod';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { p as ToolDefinition, q as ToolCall, r as Message } from './types-5PAjLoCp.cjs';
1
+ import { l as ToolDefinition, i as ToolCall, n as Message } from './types-BtQexFpT.cjs';
2
2
  import { z } from 'zod';
3
3
 
4
4
  /**