@nemesis-oss/ollama-sdk 1.9.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.
- package/CHANGELOG.md +24 -0
- package/README.md +66 -1
- package/dist/{anthropic-5oYIRS6D.d.ts → anthropic-CPaoxeLX.d.ts} +1 -1
- package/dist/{anthropic-BIh45UXj.d.cts → anthropic-DuE8M6Tz.d.cts} +1 -1
- package/dist/{compose-DCVYMvOL.d.ts → compose-BQYXdpar.d.ts} +1 -1
- package/dist/{compose-Dj6FdF2n.d.cts → compose-lt-Qz3lI.d.cts} +1 -1
- package/dist/generated-api.d.cts +5 -5
- package/dist/generated-api.d.ts +5 -5
- package/dist/generated-runtime.d.cts +2 -2
- package/dist/generated-runtime.d.ts +2 -2
- package/dist/index.cjs +260 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +203 -9
- package/dist/index.d.ts +203 -9
- package/dist/index.js +256 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp-generated.d.cts +2 -2
- package/dist/mcp-generated.d.ts +2 -2
- package/dist/{runtime-C3rfpkZw.d.ts → runtime-BV9SK_cf.d.ts} +1 -1
- package/dist/{runtime-D2W9TfCJ.d.cts → runtime-BsdBbuOv.d.cts} +1 -1
- package/dist/skills.d.cts +3 -3
- package/dist/skills.d.ts +3 -3
- package/dist/{types-DVcX_zMp.d.cts → types-BtQexFpT.d.cts} +5 -2
- package/dist/{types-DVcX_zMp.d.ts → types-BtQexFpT.d.ts} +5 -2
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
3
27
|
## [1.9.0] — Digest upgrades: vision resolver, dual-mode Responses bridge, KV-cache sessions, context safety, blob publishing, cloud usage & balance
|
|
4
28
|
|
|
5
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.
|
package/README.md
CHANGED
|
@@ -24,11 +24,13 @@
|
|
|
24
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
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
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.
|
|
27
29
|
- 🌊 **Web Stream Adapters**: Drop-in adapters (`toTextStream`, `toDataStream`, `toResponse`) for Next.js Route Handlers and Vercel AI SDK.
|
|
28
30
|
- 📈 **OpenTelemetry Instrumentation**: Automatic spans for HTTP requests, endpoint failover, chat/generate calls, and agent runs — zero-cost when OpenTelemetry isn't installed.
|
|
29
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.
|
|
30
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.
|
|
31
|
-
- 📦 **Dual ESM & CJS Build**: Full module support with
|
|
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.
|
|
32
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).
|
|
33
35
|
|
|
34
36
|
---
|
|
@@ -462,6 +464,31 @@ console.log(
|
|
|
462
464
|
|
|
463
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`.
|
|
464
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
|
+
|
|
465
492
|
### Model Lifecycle Management
|
|
466
493
|
|
|
467
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)):
|
|
@@ -962,6 +989,37 @@ initial HTTP round trip:
|
|
|
962
989
|
between turns, outside any `chat()` call, so a slow tool never ties up one of your
|
|
963
990
|
scarce concurrent-request accounts.
|
|
964
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
|
+
|
|
965
1023
|
---
|
|
966
1024
|
|
|
967
1025
|
### Observability with OpenTelemetry
|
|
@@ -1356,6 +1414,13 @@ npm run bench
|
|
|
1356
1414
|
|
|
1357
1415
|
# Run full CI verification pipeline (typecheck, lint, test, build, edge runtime check)
|
|
1358
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
|
|
1359
1424
|
```
|
|
1360
1425
|
|
|
1361
1426
|
---
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, ae as SseEvent, x as OllamaClientError } from './runtime-
|
|
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.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { O as OllamaRuntime, H as HttpClient, A as AbortableAsyncIterable, ae as SseEvent, x as OllamaClientError } from './runtime-
|
|
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.
|
package/dist/generated-api.d.cts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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-
|
|
5
|
-
import './types-
|
|
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-DuE8M6Tz.cjs';
|
|
2
|
+
export { N as NativeApi } from './anthropic-DuE8M6Tz.cjs';
|
|
3
|
+
import { O as OllamaRuntime } from './runtime-BsdBbuOv.cjs';
|
|
4
|
+
export { a as OperationDefinition } from './runtime-BsdBbuOv.cjs';
|
|
5
|
+
import './types-BtQexFpT.cjs';
|
|
6
6
|
import 'zod';
|
|
7
7
|
|
|
8
8
|
/**
|
package/dist/generated-api.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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-
|
|
5
|
-
import './types-
|
|
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-CPaoxeLX.js';
|
|
2
|
+
export { N as NativeApi } from './anthropic-CPaoxeLX.js';
|
|
3
|
+
import { O as OllamaRuntime } from './runtime-BV9SK_cf.js';
|
|
4
|
+
export { a as OperationDefinition } from './runtime-BV9SK_cf.js';
|
|
5
|
+
import './types-BtQexFpT.js';
|
|
6
6
|
import 'zod';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { I as InvokeRequest, O as OllamaRuntime, b as OllamaRuntimeOptions, a as OperationDefinition, S as SchemaRef } from './runtime-
|
|
1
|
+
export { I as InvokeRequest, O as OllamaRuntime, b as OllamaRuntimeOptions, a as OperationDefinition, S as SchemaRef } from './runtime-BsdBbuOv.cjs';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import './types-
|
|
3
|
+
import './types-BtQexFpT.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* AUTO-GENERATED by `npm run contract:generate`. DO NOT EDIT.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { I as InvokeRequest, O as OllamaRuntime, b as OllamaRuntimeOptions, a as OperationDefinition, S as SchemaRef } from './runtime-
|
|
1
|
+
export { I as InvokeRequest, O as OllamaRuntime, b as OllamaRuntimeOptions, a as OperationDefinition, S as SchemaRef } from './runtime-BV9SK_cf.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import './types-
|
|
3
|
+
import './types-BtQexFpT.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* AUTO-GENERATED by `npm run contract:generate`. DO NOT EDIT.
|
package/dist/index.cjs
CHANGED
|
@@ -4474,6 +4474,130 @@ function contextWarningMessage(check2, kind) {
|
|
|
4474
4474
|
return `${where}: estimated prompt size ${estimate} outright exceeds Ollama's conservative server-default context window (${OLLAMA_FALLBACK_CONTEXT_LENGTH}\u20134096 tokens when \`num_ctx\` is unset \u2014 model-dependent). Unless the model's default is larger, the server will silently truncate the top of the conversation \u2014 the model loses earlier turns with no error. Set \`options.num_ctx\` (or the client's \`defaultContextLength\`) to an explicit window to make truncation visible and controllable. Estimates are heuristic (\xB120\u201330%); see \`estimateTokens\`.`;
|
|
4475
4475
|
}
|
|
4476
4476
|
|
|
4477
|
+
// src/embed-batch.ts
|
|
4478
|
+
var DEFAULT_EMBED_BATCH_SIZE = 32;
|
|
4479
|
+
var DEFAULT_EMBED_CONCURRENCY = 3;
|
|
4480
|
+
function findOversizedEmbedInputs(input, contextLength) {
|
|
4481
|
+
const oversized = [];
|
|
4482
|
+
for (let index = 0; index < input.length; index++) {
|
|
4483
|
+
const text = input[index];
|
|
4484
|
+
if (text === void 0) continue;
|
|
4485
|
+
const estimate = estimateTokens(text);
|
|
4486
|
+
if (estimate > contextLength) {
|
|
4487
|
+
oversized.push({ index, estimate });
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
return oversized;
|
|
4491
|
+
}
|
|
4492
|
+
function embedBatchOverflowMessage(oversized, totalInputs, contextLength, truncateFalse) {
|
|
4493
|
+
const shown = oversized.slice(0, 5).map((entry) => `#${entry.index} (~${entry.estimate} tokens)`);
|
|
4494
|
+
const elided = oversized.length > 5 ? `, +${oversized.length - 5} more` : "";
|
|
4495
|
+
const outcome = truncateFalse ? "the server will reject them (`truncate: false`)" : "Ollama will silently truncate them, producing degraded vectors";
|
|
4496
|
+
return `embedBatch: ${oversized.length} of ${totalInputs} input strings exceed the ${contextLength}-token context window (${shown.join(", ")}${elided}). ${outcome}. Raise \`options.num_ctx\`, pre-split long inputs, or set \`truncate: true\` explicitly to acknowledge truncation.`;
|
|
4497
|
+
}
|
|
4498
|
+
function assertPositiveInteger(value, field) {
|
|
4499
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
4500
|
+
throw new OllamaClientError(
|
|
4501
|
+
`embedBatch: \`${field}\` must be a positive integer (got ${value}).`,
|
|
4502
|
+
{ code: "invalid_request" }
|
|
4503
|
+
);
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
async function batchEmbed(client, options) {
|
|
4507
|
+
const { model, input } = options;
|
|
4508
|
+
const batchSize = options.batchSize ?? DEFAULT_EMBED_BATCH_SIZE;
|
|
4509
|
+
const concurrency = options.concurrency ?? DEFAULT_EMBED_CONCURRENCY;
|
|
4510
|
+
assertPositiveInteger(batchSize, "batchSize");
|
|
4511
|
+
assertPositiveInteger(concurrency, "concurrency");
|
|
4512
|
+
if (input.length === 0) {
|
|
4513
|
+
return { model, embeddings: [], batchCount: 0 };
|
|
4514
|
+
}
|
|
4515
|
+
const batches = [];
|
|
4516
|
+
for (let start = 0; start < input.length; start += batchSize) {
|
|
4517
|
+
batches.push(input.slice(start, start + batchSize));
|
|
4518
|
+
}
|
|
4519
|
+
const embeddings = new Array(input.length);
|
|
4520
|
+
const controller = new AbortController();
|
|
4521
|
+
const propagateExternalAbort = () => {
|
|
4522
|
+
controller.abort(options.signal?.reason);
|
|
4523
|
+
};
|
|
4524
|
+
if (options.signal) {
|
|
4525
|
+
if (options.signal.aborted) {
|
|
4526
|
+
propagateExternalAbort();
|
|
4527
|
+
} else {
|
|
4528
|
+
options.signal.addEventListener("abort", propagateExternalAbort, { once: true });
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
let nextBatch = 0;
|
|
4532
|
+
let completedBatches = 0;
|
|
4533
|
+
let failure;
|
|
4534
|
+
const worker = async () => {
|
|
4535
|
+
for (; ; ) {
|
|
4536
|
+
if (controller.signal.aborted) {
|
|
4537
|
+
throw failure?.error ?? new OllamaAbortError("Embed batch aborted");
|
|
4538
|
+
}
|
|
4539
|
+
const batchIndex = nextBatch;
|
|
4540
|
+
nextBatch++;
|
|
4541
|
+
if (batchIndex >= batches.length) return;
|
|
4542
|
+
const batch = batches[batchIndex];
|
|
4543
|
+
if (!batch) return;
|
|
4544
|
+
try {
|
|
4545
|
+
const request = {
|
|
4546
|
+
model,
|
|
4547
|
+
input: batch,
|
|
4548
|
+
...options.truncate !== void 0 ? { truncate: options.truncate } : {},
|
|
4549
|
+
...options.dimensions !== void 0 ? { dimensions: options.dimensions } : {},
|
|
4550
|
+
...options.keep_alive !== void 0 ? { keep_alive: options.keep_alive } : {},
|
|
4551
|
+
...options.options !== void 0 ? { options: options.options } : {},
|
|
4552
|
+
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
4553
|
+
signal: controller.signal
|
|
4554
|
+
};
|
|
4555
|
+
const response = await client.embed(request);
|
|
4556
|
+
if (response.embeddings.length !== batch.length) {
|
|
4557
|
+
throw new OllamaClientError(
|
|
4558
|
+
`embedBatch: server returned ${response.embeddings.length} embeddings for a batch of ${batch.length} inputs (batch ${batchIndex}).`,
|
|
4559
|
+
{ code: "invalid_response" }
|
|
4560
|
+
);
|
|
4561
|
+
}
|
|
4562
|
+
const offset = batchIndex * batchSize;
|
|
4563
|
+
for (let j = 0; j < batch.length; j++) {
|
|
4564
|
+
embeddings[offset + j] = response.embeddings[j];
|
|
4565
|
+
}
|
|
4566
|
+
completedBatches++;
|
|
4567
|
+
options.onBatchComplete?.(completedBatches, batches.length);
|
|
4568
|
+
} catch (error) {
|
|
4569
|
+
if (!failure) {
|
|
4570
|
+
failure = { error };
|
|
4571
|
+
controller.abort(error);
|
|
4572
|
+
throw error;
|
|
4573
|
+
}
|
|
4574
|
+
throw failure.error;
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
};
|
|
4578
|
+
try {
|
|
4579
|
+
const workerCount = Math.min(concurrency, batches.length);
|
|
4580
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
4581
|
+
} finally {
|
|
4582
|
+
if (options.signal) {
|
|
4583
|
+
options.signal.removeEventListener("abort", propagateExternalAbort);
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
for (let i = 0; i < embeddings.length; i++) {
|
|
4587
|
+
if (embeddings[i] === void 0) {
|
|
4588
|
+
throw new OllamaClientError(
|
|
4589
|
+
`embedBatch: internal invariant violated \u2014 no embedding produced for input #${i}.`,
|
|
4590
|
+
{ code: "invalid_response" }
|
|
4591
|
+
);
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
return {
|
|
4595
|
+
model,
|
|
4596
|
+
embeddings,
|
|
4597
|
+
batchCount: batches.length
|
|
4598
|
+
};
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4477
4601
|
// src/generated/api/operations.ts
|
|
4478
4602
|
var blobsOp = {
|
|
4479
4603
|
operationId: "blobs",
|
|
@@ -4998,6 +5122,14 @@ var OllamaClient = class {
|
|
|
4998
5122
|
contextWarningThreshold;
|
|
4999
5123
|
/** Configured overflow policy — `'warn'` (default) or `'throw'`. */
|
|
5000
5124
|
onContextOverflow;
|
|
5125
|
+
/**
|
|
5126
|
+
* Controllers for every in-flight request and active stream managed by this
|
|
5127
|
+
* client — see {@link OllamaClient.destroy} and {@link createRequestScope}.
|
|
5128
|
+
* Entries live for exactly as long as the underlying work does: until the
|
|
5129
|
+
* response settles (requests) or the stream's `finalResult` settles
|
|
5130
|
+
* (streams, via `executeWithFailover`'s `holdUntil` release path).
|
|
5131
|
+
*/
|
|
5132
|
+
activeRequests = /* @__PURE__ */ new Set();
|
|
5001
5133
|
constructor(config = {}) {
|
|
5002
5134
|
this.cloudApiKey = resolveApiKey(config.apiKey);
|
|
5003
5135
|
const defaultBaseUrl = resolveBaseUrl(config.baseUrl);
|
|
@@ -5162,6 +5294,39 @@ var OllamaClient = class {
|
|
|
5162
5294
|
}
|
|
5163
5295
|
return { ...options, num_ctx: this.defaultContextLength };
|
|
5164
5296
|
}
|
|
5297
|
+
/**
|
|
5298
|
+
* Creates the per-request abort scope registered with {@link destroy}:
|
|
5299
|
+
* a controller that fires when the caller's `signal` aborts OR when
|
|
5300
|
+
* `destroy()` runs, whichever comes first. `dispose()` removes the
|
|
5301
|
+
* registration when the work it guards settles — for plain requests when
|
|
5302
|
+
* the response settles, for streams when `finalResult` settles (the same
|
|
5303
|
+
* moment `executeWithFailover`'s `holdUntil` releases the endpoint slot),
|
|
5304
|
+
* so a destroy() can tear down a stream mid-consumption but never outlives
|
|
5305
|
+
* one that already finished.
|
|
5306
|
+
*/
|
|
5307
|
+
createRequestScope(userSignal) {
|
|
5308
|
+
const controller = new AbortController();
|
|
5309
|
+
const propagateUserAbort = () => {
|
|
5310
|
+
controller.abort(userSignal?.reason);
|
|
5311
|
+
};
|
|
5312
|
+
let detach;
|
|
5313
|
+
if (userSignal) {
|
|
5314
|
+
if (userSignal.aborted) {
|
|
5315
|
+
propagateUserAbort();
|
|
5316
|
+
} else {
|
|
5317
|
+
userSignal.addEventListener("abort", propagateUserAbort, { once: true });
|
|
5318
|
+
detach = () => userSignal.removeEventListener("abort", propagateUserAbort);
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
this.activeRequests.add(controller);
|
|
5322
|
+
return {
|
|
5323
|
+
signal: controller.signal,
|
|
5324
|
+
dispose: () => {
|
|
5325
|
+
detach?.();
|
|
5326
|
+
this.activeRequests.delete(controller);
|
|
5327
|
+
}
|
|
5328
|
+
};
|
|
5329
|
+
}
|
|
5165
5330
|
/**
|
|
5166
5331
|
* Fail-fast guard for `format` (structured output) requests: throws before any network
|
|
5167
5332
|
* call if the candidate endpoint is inferred as Ollama Cloud, which does not currently
|
|
@@ -5178,7 +5343,8 @@ var OllamaClient = class {
|
|
|
5178
5343
|
}
|
|
5179
5344
|
}
|
|
5180
5345
|
async executeWithFailover(operation, options) {
|
|
5181
|
-
const
|
|
5346
|
+
const scope = this.createRequestScope(options?.signal);
|
|
5347
|
+
const timeout = createTimeoutSignal(options?.timeoutMs ?? this.timeoutMs, scope.signal);
|
|
5182
5348
|
const requestId = createLogicalRequestId();
|
|
5183
5349
|
let deferTimeoutCancel = false;
|
|
5184
5350
|
try {
|
|
@@ -5257,6 +5423,7 @@ var OllamaClient = class {
|
|
|
5257
5423
|
if (holdPromise) {
|
|
5258
5424
|
void holdPromise.catch(() => void 0).finally(() => {
|
|
5259
5425
|
timeout.cancel();
|
|
5426
|
+
scope.dispose();
|
|
5260
5427
|
this.registry.release(endpoint.name);
|
|
5261
5428
|
});
|
|
5262
5429
|
} else {
|
|
@@ -5267,7 +5434,10 @@ var OllamaClient = class {
|
|
|
5267
5434
|
throw lastError ?? new Error("No healthy Ollama endpoints available");
|
|
5268
5435
|
}
|
|
5269
5436
|
} finally {
|
|
5270
|
-
if (!deferTimeoutCancel)
|
|
5437
|
+
if (!deferTimeoutCancel) {
|
|
5438
|
+
timeout.cancel();
|
|
5439
|
+
scope.dispose();
|
|
5440
|
+
}
|
|
5271
5441
|
}
|
|
5272
5442
|
}
|
|
5273
5443
|
async chat(req) {
|
|
@@ -5427,6 +5597,58 @@ var OllamaClient = class {
|
|
|
5427
5597
|
const res = await this.embed({ model, input });
|
|
5428
5598
|
return res.embeddings;
|
|
5429
5599
|
}
|
|
5600
|
+
/**
|
|
5601
|
+
* Batch-constrained, order-preserving embedding generation for large
|
|
5602
|
+
* corpora — the safe building block for RAG/vector-index ingestion at
|
|
5603
|
+
* scale. Splits `input` into `batchSize` slices and embeds them through
|
|
5604
|
+
* {@link embed} with at most `concurrency` batches in flight (defaults:
|
|
5605
|
+
* 32 × 3), so `Promise.all`-style floods that OOM local daemons and
|
|
5606
|
+
* saturate `OLLAMA_MAX_QUEUE` can't happen by accident. Fail-fast: the
|
|
5607
|
+
* first batch error aborts every sibling in-flight batch and rejects the
|
|
5608
|
+
* whole operation with the original error.
|
|
5609
|
+
*
|
|
5610
|
+
* Before any request is sent, `defaultContextLength` is injected as
|
|
5611
|
+
* `num_ctx` (same as chat/generate) and each input string's token estimate
|
|
5612
|
+
* is checked against the resolved window: Ollama silently truncates
|
|
5613
|
+
* oversized embedding inputs by default, so the client warns (or throws,
|
|
5614
|
+
* under `onContextOverflow: 'throw'`) with the offending indexes — see
|
|
5615
|
+
* `src/embed-batch.ts` for the exact trip point (no margin: an embedding
|
|
5616
|
+
* prompt IS the whole input).
|
|
5617
|
+
*
|
|
5618
|
+
* ```ts
|
|
5619
|
+
* const { embeddings, batchCount } = await client.embedBatch({
|
|
5620
|
+
* model: 'nomic-embed-text:latest',
|
|
5621
|
+
* input: corpus, // e.g. 50k chunks
|
|
5622
|
+
* batchSize: 32, // inputs per /api/embed request
|
|
5623
|
+
* concurrency: 3, // batches in flight
|
|
5624
|
+
* onBatchComplete: (done, total) => progress.log(`${done}/${total}`),
|
|
5625
|
+
* });
|
|
5626
|
+
* // embeddings[i] always corresponds to corpus[i]
|
|
5627
|
+
* ```
|
|
5628
|
+
*/
|
|
5629
|
+
async embedBatch(options) {
|
|
5630
|
+
const resolvedOptions = this.injectDefaultContextLength(options.options);
|
|
5631
|
+
const contextLength = resolvedOptions?.num_ctx;
|
|
5632
|
+
if (contextLength !== void 0) {
|
|
5633
|
+
const oversized = findOversizedEmbedInputs(options.input, contextLength);
|
|
5634
|
+
if (oversized.length > 0) {
|
|
5635
|
+
const message = embedBatchOverflowMessage(
|
|
5636
|
+
oversized,
|
|
5637
|
+
options.input.length,
|
|
5638
|
+
contextLength,
|
|
5639
|
+
options.truncate === false
|
|
5640
|
+
);
|
|
5641
|
+
if (this.onContextOverflow === "throw") {
|
|
5642
|
+
throw new OllamaClientError(message, { code: "context_overflow" });
|
|
5643
|
+
}
|
|
5644
|
+
this.logger.warn(message);
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
return batchEmbed(
|
|
5648
|
+
this,
|
|
5649
|
+
resolvedOptions === options.options ? options : { ...options, options: resolvedOptions }
|
|
5650
|
+
);
|
|
5651
|
+
}
|
|
5430
5652
|
/**
|
|
5431
5653
|
* @deprecated Ollama's `/api/embeddings` endpoint has been superseded by `/api/embed`
|
|
5432
5654
|
* (exposed here as {@link OllamaClient.embed}), which additionally supports batch
|
|
@@ -5596,10 +5818,12 @@ var OllamaClient = class {
|
|
|
5596
5818
|
async executeCloudRequest(operation, options) {
|
|
5597
5819
|
var _stack = [];
|
|
5598
5820
|
try {
|
|
5599
|
-
const
|
|
5821
|
+
const scope = this.createRequestScope(options.signal);
|
|
5822
|
+
const timeout = createTimeoutSignal(options.timeoutMs ?? this.timeoutMs, scope.signal);
|
|
5600
5823
|
const _timeout = __using(_stack, {
|
|
5601
5824
|
[Symbol.dispose]() {
|
|
5602
5825
|
timeout.cancel();
|
|
5826
|
+
scope.dispose();
|
|
5603
5827
|
}
|
|
5604
5828
|
});
|
|
5605
5829
|
const requestId = createLogicalRequestId();
|
|
@@ -5660,6 +5884,34 @@ var OllamaClient = class {
|
|
|
5660
5884
|
endpointStatus() {
|
|
5661
5885
|
return this.registry.status();
|
|
5662
5886
|
}
|
|
5887
|
+
/**
|
|
5888
|
+
* Immediately aborts every in-flight request and active stream managed by
|
|
5889
|
+
* this client — the clean teardown path for short-lived processes, CLI
|
|
5890
|
+
* runners, and `node:worker_threads` tasks where dangling fetch bodies,
|
|
5891
|
+
* unconsumed stream readers, or queued capacity waiters would otherwise
|
|
5892
|
+
* keep the event loop (and thus the thread/process) alive.
|
|
5893
|
+
*
|
|
5894
|
+
* Every aborted operation rejects with an {@link OllamaAbortError}
|
|
5895
|
+
* (`code: 'aborted'`) carrying `reason` as its message — including requests
|
|
5896
|
+
* still queued behind `maxConcurrentPerEndpoint`, not just dispatched ones.
|
|
5897
|
+
* Streams reject their `finalResult` and release their endpoint slots.
|
|
5898
|
+
*
|
|
5899
|
+
* Idempotent: calling it again (or on a client with nothing in flight)
|
|
5900
|
+
* is a no-op that returns `0`. The client remains usable afterward —
|
|
5901
|
+
* `destroy()` is a drain, not a permanent disable; callers that want a
|
|
5902
|
+
* permanently-dead client should discard the instance.
|
|
5903
|
+
*
|
|
5904
|
+
* @returns the number of in-flight operations that were aborted.
|
|
5905
|
+
*/
|
|
5906
|
+
destroy(reason = "Ollama client destroyed") {
|
|
5907
|
+
const controllers = [...this.activeRequests];
|
|
5908
|
+
const cause = new OllamaAbortError(reason);
|
|
5909
|
+
for (const controller of controllers) {
|
|
5910
|
+
controller.abort(cause);
|
|
5911
|
+
}
|
|
5912
|
+
this.activeRequests.clear();
|
|
5913
|
+
return controllers.length;
|
|
5914
|
+
}
|
|
5663
5915
|
// --- Compatibility Adapters ---
|
|
5664
5916
|
get openai() {
|
|
5665
5917
|
const ep = this.registry.candidates()[0];
|
|
@@ -6821,6 +7073,8 @@ exports.ConversationSession = ConversationSession;
|
|
|
6821
7073
|
exports.DEFAULT_BACKOFF = DEFAULT_BACKOFF;
|
|
6822
7074
|
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
6823
7075
|
exports.DEFAULT_CONTEXT_WARNING_THRESHOLD = DEFAULT_CONTEXT_WARNING_THRESHOLD;
|
|
7076
|
+
exports.DEFAULT_EMBED_BATCH_SIZE = DEFAULT_EMBED_BATCH_SIZE;
|
|
7077
|
+
exports.DEFAULT_EMBED_CONCURRENCY = DEFAULT_EMBED_CONCURRENCY;
|
|
6824
7078
|
exports.DEFAULT_FAILOVER_CODES = DEFAULT_FAILOVER_CODES;
|
|
6825
7079
|
exports.DEFAULT_FETCH_BACKOFF_CONFIG = DEFAULT_FETCH_BACKOFF_CONFIG;
|
|
6826
7080
|
exports.DEFAULT_RETRY_CONFIG = DEFAULT_RETRY_CONFIG;
|
|
@@ -6882,6 +7136,7 @@ exports.ToolCallAccumulator = ToolCallAccumulator;
|
|
|
6882
7136
|
exports.ToolRegistry = ToolRegistry;
|
|
6883
7137
|
exports.addSpanEvent = addSpanEvent;
|
|
6884
7138
|
exports.applySkill = applySkill;
|
|
7139
|
+
exports.batchEmbed = batchEmbed;
|
|
6885
7140
|
exports.calculateBackoff = calculateBackoff;
|
|
6886
7141
|
exports.checkChatContext = checkChatContext;
|
|
6887
7142
|
exports.checkEndpointHealth = checkEndpointHealth;
|
|
@@ -6894,6 +7149,7 @@ exports.createOllamaCloudFreeTierQuota = createOllamaCloudFreeTierQuota;
|
|
|
6894
7149
|
exports.createTimeoutSignal = createTimeoutSignal;
|
|
6895
7150
|
exports.defineTool = defineTool;
|
|
6896
7151
|
exports.detectModelCapabilities = detectModelCapabilities;
|
|
7152
|
+
exports.embedBatchOverflowMessage = embedBatchOverflowMessage;
|
|
6897
7153
|
exports.encodeImage = encodeImage;
|
|
6898
7154
|
exports.estimateChatRequestTokens = estimateChatRequestTokens;
|
|
6899
7155
|
exports.estimateGenerateRequestTokens = estimateGenerateRequestTokens;
|
|
@@ -6901,6 +7157,7 @@ exports.estimateSystemOneRequestBytes = estimateSystemOneRequestBytes;
|
|
|
6901
7157
|
exports.estimateTokens = estimateTokens;
|
|
6902
7158
|
exports.extractUsage = extractUsage;
|
|
6903
7159
|
exports.fetchWithBackoff = fetchWithBackoff;
|
|
7160
|
+
exports.findOversizedEmbedInputs = findOversizedEmbedInputs;
|
|
6904
7161
|
exports.imageStringNeedsResolution = imageStringNeedsResolution;
|
|
6905
7162
|
exports.inferRuntimeMode = inferRuntimeMode;
|
|
6906
7163
|
exports.isKeepAliveSugar = isKeepAliveSugar;
|