@nemesis-oss/ollama-sdk 1.19.0 → 1.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.19.1] — Honoring `Retry-After` on 429s: closing the dead-`retryAfterMs` gap (audit v1.19.0 parity)
4
+
5
+ The daily audit against the v1.19.0 baseline (`2d3ec47`) claimed six capability domains all PASS; point-by-point verification found five true and one false — "Native client methods with `OllamaRateLimitError.retryAfterMs` and jittered 429 retries". The field existed and the docs already described it, but nothing populated or consumed it: all three `HttpClient` error-mapping sites discarded response headers when building error contexts, `statusToError`'s 429 branch never parsed `Retry-After`, and `withRetry` slept full-jitter backoff regardless. The pinned upstream contract documents `Retry-After` ("Seconds to wait before retrying", `type: integer`) on the 429 responses of both cloud account endpoints (`/api/usage`, `/api/balance`) — this was contract-required behavior our own `docs-site/api/errors.md` already claimed. All changes live in the handwritten layer; no contract or generated-surface changes (see [ADR 0035](./docs/adr/0035-retry-after-honoring.md)). 1057 pre-existing tests stay green, 30 net-new tests added.
6
+
7
+ ### `OllamaRateLimitError.retryAfterMs` is now real
8
+
9
+ - **`parseRetryAfterMs(headerValue, now?)` — new root-exported parser**: delay-seconds (`"120"` → 120 000 ms) or strict IMF-fixdate HTTP-date (→ milliseconds until that instant, clamped at `0` for past dates). The HTTP-date branch validates the shape **before** `Date.parse` because engine date parsers are dangerously lenient (V8 accepts `"1.5"` as a year-2001 date); malformed or overflow values return `undefined`, which falls back to jitter instead of overriding it with a bogus delay. `lookupHeader` (module-level export) does the case-insensitive header lookup on error-context header records.
10
+ - `statusToError`'s 429 branch injects `retryAfterMs` from the response headers — one mapping chokepoint, one status. No other status mapping parses the header: 503/502 keep jitter, because nothing documents `Retry-After` for them (the same invented-behavior reasoning ADR 0034 § 5 applied to the cloud limits).
11
+
12
+ ### Transport capture and retry pacing
13
+
14
+ - `HttpClient` now captures `retry-after` at all three error-mapping sites (`request()`, `requestStream()`, `requestSseStream()`) and the no-middleware lifecycle error event (the middleware path already reported headers) — deliberately capturing **only that one header**, so sensitive fields like `set-cookie` never leak into thrown error objects that callers log. The capture is defensive by design: mock `Response` shapes without a `Headers` implementation degrade to the pre-1.19.1 error-context shape instead of throwing mid-enrichment.
15
+ - **`withRetry` honors the server's delay verbatim** — no jitter, no clamping to `maxDelayMs` — via the new `retryDelayMs(error, attempt, backoff)`: the server explicitly said when the client may retry, and re-issuing earlier would only compound the rate limiting the 429 exists to prevent. Callers who consider a server value excessive keep three sanctioned exits: `shouldRetry` rejecting the error, the abort signal (live for the whole sleep), or a `maxRetries` budget. Absent/unparseable headers — and every other retryable error — keep the full-jitter exponential backoff, unchanged. `onRetry` reports the delay actually used, either way.
16
+ - `fetchWithBackoff` honors `Retry-After` on 429s identically — its own docs claim "the same retry discipline as the SDK's internal transport", which is kept honest rather than re-worded. The honoring is 429-scoped: 503s keep full jitter even if a header is present.
17
+
18
+ ### Docs
19
+
20
+ - New [ADR 0035](./docs/adr/0035-retry-after-honoring.md) recording the fix and the declined alternatives with reasoning (clamping to `maxDelayMs`, capturing the full header record, extending `Retry-After` parsing to 503/502, jittering the server value). ADR indexes updated: `docs/adr/README.md` and `docs-site/adr/index.md` — the latter also gains its missing 0034 row from the 1.19.0 wave.
21
+ - `docs-site/api/errors.md`'s `OllamaRateLimitError` section now describes the honored-by-`withRetry` semantics and the `parseRetryAfterMs` export; `client.ts`'s cloud-endpoint JSDoc (`webSearch`/`webFetch`/`usage`/`balance`) notes the contract-declared `Retry-After` on 429s.
22
+
3
23
  ## [1.19.0] — System One cloud launch contract refresh: dual request-size cap, 402 taxonomy, re-pin d9b8601
4
24
 
5
25
  The scheduled upstream snapshot freshness check (offered after the Task 24 docs fix, accepted) found upstream `openapi.yaml@main` had moved from `2a9ffe3` to `d9b8601` with the **System One cloud launch** — audited point-by-point against the pinned snapshot before any change, per house discipline (see [ADR 0034](./docs/adr/0034-systemone-cloud-launch-contract-refresh.md)). Upstream now declares `/v1/systemone` with local **and cloud** servers (`https://ollama.com`, optional `bearerAuth`), the `model` field accepts cloud models (`clef:cloud`, requiring Ollama >= 0.40.5 or the hosted endpoint), the local byte caps were **clarified from an either/or reading to a dual bound**, and 401/402 joined the documented response set. A second discovery retired an old deviation: the "corrupted Usage enum literals" the 2026-10-07 pin repaired were our own Bash display artifact eating `[24h`/`[h` CSI-style sequences — upstream serves them clean inline, so the repair deviation is retired. All changes additive or contract-aligning; no breaking API changes. 1047 pre-existing tests stay green, 10 net-new tests added (5 rewritten onto the new semantics).
package/README.md CHANGED
@@ -337,7 +337,7 @@ Ollama implements the Responses API **non-statefully** — `previous_response_id
337
337
 
338
338
  ### Context-Window Safety (silent-truncation guard)
339
339
 
340
- 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:
340
+ With `num_ctx` unset, Ollama sizes the default window by available VRAM — 4,096 tokens below 24 GiB, 32,768 at 24–48 GiB, 262,144 at 48 GiB and up (daemon-wide override via `OLLAMA_CONTEXT_LENGTH`; cloud models run at their maximum) — and **silently truncates** prompts that don't fit. The client estimates the prompt size before every `chat`/`generate` call and surfaces the risk:
341
341
 
342
342
  ```typescript
343
343
  const client = new OllamaClient({
@@ -359,7 +359,7 @@ The estimators are exported for custom pipelines: `estimateTokens(text)` (CJK-aw
359
359
 
360
360
  ### Discovering the Real Context Window
361
361
 
362
- Guessing context windows ("it's probably 2048… or 4096?") is how silent truncation happens. Ollama advertises the real numbers in two places, and they answer _different questions_: `GET /api/ps` reports the window the **running instance actually allocated** (exact, reflects the Modelfile `num_ctx` default and what fit in memory), while `POST /api/show` exposes the Modelfile's `num_ctx` default _and_ the model's **native GGUF maximum** in `model_info["<architecture>.context_length"]` — the official API examples show a model with a 131072-token native max whose running instance allocated 4096, a 32× gap. `getContextLength()` consults them in precedence order and returns every signal it found:
362
+ Guessing context windows ("it's probably 2048… or 4096?") is how silent truncation happens — worse, current daemons tier the unset-`num_ctx` default by VRAM (4k / 32k / 256k), so a hardcoded guess is now wrong in both directions. Ollama advertises the real numbers in two places, and they answer _different questions_: `GET /api/ps` reports the window the **running instance actually allocated** (exact, reflects the Modelfile `num_ctx` default and what fit in memory), while `POST /api/show` exposes the Modelfile's `num_ctx` default _and_ the model's **native GGUF maximum** in `model_info["<architecture>.context_length"]` — the official API examples show a model with a 131072-token native max whose running instance allocated 4096, a 32× gap. `getContextLength()` consults them in precedence order and returns every signal it found:
363
363
 
364
364
  ```typescript
365
365
  const window = await client.models.getContextLength({ model: 'gemma4' });
@@ -467,12 +467,12 @@ Dispatch is **affinity-sticky, not round-robin**: when a queued model is already
467
467
 
468
468
  The server-side knobs (official FAQ defaults) and the client-side surfaces that map onto them:
469
469
 
470
- | Server knob | Default | Effect | Client-side counterpart |
471
- | ----------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
472
- | `OLLAMA_NUM_PARALLEL` | 1 | Parallel requests **per loaded model**; required memory scales `NUM_PARALLEL × CONTEXT_LENGTH` (a 4K-context model at 4 parallel allocates a 16K KV cache) | `ModelAffinityScheduler({ perModelConcurrency })`, `ToolRegistry({ maxConcurrency })` for agent tool dispatch |
473
- | `OLLAMA_MAX_LOADED_MODELS` | 3× GPUs (3 on CPU) | Concurrently loaded models; beyond it, requests queue until a model goes idle or is evicted — the swap/thrash cycle | `ModelAffinityScheduler({ concurrentModels })` — keep the working set small; `client.models.pin()`/`unload()` for explicit lifecycle |
474
- | `OLLAMA_MAX_QUEUE` | 512 | Queued requests beyond this are rejected immediately with **503** | `endpointHealth.maxConcurrentPerEndpoint` (queue client-side instead of saturating the daemon), `embedBatch({ batchSize, concurrency })` for ingestion, retry/failover on 503 |
475
- | `OLLAMA_CONTEXT_LENGTH` / `num_ctx` | 2048–4096 model-dependent | The window per request — prompts beyond it are **silently truncated** | `models.getContextLength()` for the real number, `defaultContextLength` to make it explicit, pre-flight warnings + `session.compact()` to stay inside it |
470
+ | Server knob | Default | Effect | Client-side counterpart |
471
+ | ----------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
472
+ | `OLLAMA_NUM_PARALLEL` | 1 | Parallel requests **per loaded model**; required memory scales `NUM_PARALLEL × CONTEXT_LENGTH` (a 4K-context model at 4 parallel allocates a 16K KV cache) | `ModelAffinityScheduler({ perModelConcurrency })`, `ToolRegistry({ maxConcurrency })` for agent tool dispatch |
473
+ | `OLLAMA_MAX_LOADED_MODELS` | 3× GPUs (3 on CPU) | Concurrently loaded models; beyond it, requests queue until a model goes idle or is evicted — the swap/thrash cycle | `ModelAffinityScheduler({ concurrentModels })` — keep the working set small; `client.models.pin()`/`unload()` for explicit lifecycle |
474
+ | `OLLAMA_MAX_QUEUE` | 512 | Queued requests beyond this are rejected immediately with **503** | `endpointHealth.maxConcurrentPerEndpoint` (queue client-side instead of saturating the daemon), `embedBatch({ batchSize, concurrency })` for ingestion, retry/failover on 503 |
475
+ | `OLLAMA_CONTEXT_LENGTH` / `num_ctx` | VRAM-tiered: 4k / 32k / 256k | The window per request — prompts beyond it are **silently truncated** | `models.getContextLength()` for the real number, `defaultContextLength` to make it explicit, pre-flight warnings + `session.compact()` to stay inside it |
476
476
 
477
477
  The two failure modes this table prevents: `Promise.all`-style floods (hundreds of simultaneous requests → queue saturation → 503s, VRAM spikes) and model ping-pong (alternating models → repeated unload/reload stalls). The SDK's request path already caps and queues per endpoint via `endpointHealth` — including `strategy: 'least-connections'` for pools — and `embedBatch`/`ModelAffinityScheduler` extend that discipline to ingestion and multi-model workloads. `OLLAMA_KV_CACHE_TYPE` (`f16`/`q8_0`/`q4_0`) trades KV precision for memory headroom at the daemon level; benchmark its latency impact against these client-side levers before relying on it for throughput.
478
478
 
@@ -1,4 +1,4 @@
1
- import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, ah as SseEvent, z as OllamaClientError } from './runtime-DYCxYGG-.js';
1
+ import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, ah as SseEvent, z as OllamaClientError } from './runtime-CjCriL5A.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, ah as SseEvent, z as OllamaClientError } from './runtime-C28YEAHC.cjs';
1
+ import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, ah as SseEvent, z as OllamaClientError } from './runtime-EwyUaNnZ.js';
2
2
 
3
3
  /**
4
4
  * AUTO-GENERATED by `npm run contract:generate`. DO NOT EDIT.
@@ -1,7 +1,7 @@
1
- import { O as OpenAIChatCompletionRequest, a as OpenAIChatCompletionResponse, b as OpenAIChatCompletionChunk, c as OpenAICompletionRequest, d as OpenAICompletionResponse, e as OpenAICompletionChunk, f as OpenAIEmbeddingRequest, g as OpenAIEmbeddingResponse, h as OpenAIListModelsResponse, i as OpenAIModelItem, j as OpenAIResponsesRequest, k as OpenAIResponsesResponse, l as OpenAIResponsesStreamEvent, A as AnthropicMessagesRequest, m as AnthropicMessagesResponse, n as AnthropicMessageStreamEvent } from './anthropic-DIPnBwlf.cjs';
2
- export { N as NativeApi } from './anthropic-DIPnBwlf.cjs';
3
- import { O as OllamaRuntime } from './runtime-C28YEAHC.cjs';
4
- export { a as OperationDefinition } from './runtime-C28YEAHC.cjs';
1
+ import { O as OpenAIChatCompletionRequest, a as OpenAIChatCompletionResponse, b as OpenAIChatCompletionChunk, c as OpenAICompletionRequest, d as OpenAICompletionResponse, e as OpenAICompletionChunk, f as OpenAIEmbeddingRequest, g as OpenAIEmbeddingResponse, h as OpenAIListModelsResponse, i as OpenAIModelItem, j as OpenAIResponsesRequest, k as OpenAIResponsesResponse, l as OpenAIResponsesStreamEvent, A as AnthropicMessagesRequest, m as AnthropicMessagesResponse, n as AnthropicMessageStreamEvent } from './anthropic-Bf16hGLL.cjs';
2
+ export { N as NativeApi } from './anthropic-Bf16hGLL.cjs';
3
+ import { O as OllamaRuntime } from './runtime-CjCriL5A.cjs';
4
+ export { a as OperationDefinition } from './runtime-CjCriL5A.cjs';
5
5
  import './types-Y3u0FtdN.cjs';
6
6
  import 'zod';
7
7
 
@@ -1,7 +1,7 @@
1
- import { O as OpenAIChatCompletionRequest, a as OpenAIChatCompletionResponse, b as OpenAIChatCompletionChunk, c as OpenAICompletionRequest, d as OpenAICompletionResponse, e as OpenAICompletionChunk, f as OpenAIEmbeddingRequest, g as OpenAIEmbeddingResponse, h as OpenAIListModelsResponse, i as OpenAIModelItem, j as OpenAIResponsesRequest, k as OpenAIResponsesResponse, l as OpenAIResponsesStreamEvent, A as AnthropicMessagesRequest, m as AnthropicMessagesResponse, n as AnthropicMessageStreamEvent } from './anthropic-CAvwqIjx.js';
2
- export { N as NativeApi } from './anthropic-CAvwqIjx.js';
3
- import { O as OllamaRuntime } from './runtime-DYCxYGG-.js';
4
- export { a as OperationDefinition } from './runtime-DYCxYGG-.js';
1
+ import { O as OpenAIChatCompletionRequest, a as OpenAIChatCompletionResponse, b as OpenAIChatCompletionChunk, c as OpenAICompletionRequest, d as OpenAICompletionResponse, e as OpenAICompletionChunk, f as OpenAIEmbeddingRequest, g as OpenAIEmbeddingResponse, h as OpenAIListModelsResponse, i as OpenAIModelItem, j as OpenAIResponsesRequest, k as OpenAIResponsesResponse, l as OpenAIResponsesStreamEvent, A as AnthropicMessagesRequest, m as AnthropicMessagesResponse, n as AnthropicMessageStreamEvent } from './anthropic-BmFk4Pjq.js';
2
+ export { N as NativeApi } from './anthropic-BmFk4Pjq.js';
3
+ import { O as OllamaRuntime } from './runtime-EwyUaNnZ.js';
4
+ export { a as OperationDefinition } from './runtime-EwyUaNnZ.js';
5
5
  import './types-Y3u0FtdN.js';
6
6
  import 'zod';
7
7