@qvac/llm-llamacpp 0.56.0 → 0.58.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/README.md CHANGED
@@ -32,11 +32,26 @@ This native C++ addon, built using the `Bare` Runtime, simplifies running Large
32
32
  |----------|-------------|-------------|--------|-------------|
33
33
  | macOS | arm64, x64 | 14.0+ | ✅ Tier 1 | Metal |
34
34
  | iOS | arm64 | 17.0+ | ✅ Tier 1 | Metal |
35
- | Linux | arm64, x64 | Ubuntu-22+ | ✅ Tier 1 | Vulkan |
35
+ | Linux | arm64, x64 | Ubuntu-22+ | Tier 1 | CUDA (NVIDIA), Vulkan |
36
36
  | Android | arm64 | 12+ | ✅ Tier 1 | Vulkan, OpenCL (Adreno 700+) |
37
- | Windows | x64 | 10+ | ✅ Tier 1 | Vulkan |
37
+ | Windows | x64 | 10+ | Tier 1 | CUDA (NVIDIA), Vulkan |
38
38
 
39
39
 
40
+ **Note on CUDA (Linux and Windows, NVIDIA):**
41
+ The CUDA backend ships as a dynamically loaded module alongside Vulkan, and is preferred over
42
+ Vulkan when an NVIDIA device is present. On Windows it needs a CUDA 13 install: the runtime DLLs
43
+ are loaded from `%CUDA_PATH%\bin\x64`, not from `PATH`.
44
+
45
+ - If the CUDA module, driver, or required runtime DLL is missing, the device never registers and selection
46
+ falls through to Vulkan, then CPU. Nothing needs configuring for that.
47
+ - `backend: "vulkan"` forces Vulkan on an NVIDIA machine. Setting `CUDA_VISIBLE_DEVICES=-1` in the
48
+ environment has the same effect without touching the load config.
49
+ - TurboQuant / PolarQuant KV-cache types (`tbq3_0`, `tbq4_0`, `pq3_0`, `pq4_0`) are **not**
50
+ supported on CUDA and are rejected during model configuration. Use a Vulkan GPU or CPU for those.
51
+ Standard quantized types (`q4_0`, `q8_0`, …) work normally.
52
+ - BitNet (TQ1_0 / TQ2_0) and some LoRA finetuning kernels are not yet available on CUDA. Use
53
+ `backend: "vulkan"` for those workloads until the kernels land.
54
+
40
55
  **Note — BitNet models (TQ1_0 / TQ2_0 quantization):**
41
56
  BitNet models require special backend handling on Adreno GPUs. When a BitNet model is detected and no explicit `main-gpu` is set:
42
57
  - **Adreno 800+** (e.g. Adreno 830): Vulkan is used instead of OpenCL.
@@ -177,9 +192,15 @@ const config = {
177
192
  | tools | `"true"` or `"false"` | `"false"` | Enable tool calling with jinja templating |
178
193
  | verbosity | 0 – 3 (0=ERROR, 1=WARNING, 2=INFO, 3=DEBUG) | 0 | Logging verbosity level |
179
194
  | main-gpu | integer, `"integrated"`, or `"dedicated"` | — | GPU selection for multi-GPU systems |
195
+ | backend | comma-separated list of `cuda`, `vulkan`, `metal`, `opencl`, or `auto` | N/A | Overrides which GPU backend is used, in priority order (e.g. `"cuda,vulkan"`). `auto` means no preference. An unrecognised name is rejected; a recognised one with no device present is skipped. Use `device: "cpu"` to run on CPU. Cannot be combined with `split-mode` or `devices` |
180
196
  | split-mode | `"none"`, `"layer"`, or `"tensor"` | `"none"` | How to split the model across GPUs. `"tensor"` is EXPERIMENTAL and desktop-only; `"row"` is rejected ([details](./docs/multi-gpu.md)) |
181
197
  | tensor-split | comma-separated proportions (e.g. `"1,1"`) | — | GPU split ratios for the multi-GPU split modes ([details](./docs/multi-gpu.md)) |
182
198
  | parallel | integer | 1 | Concurrent sequence slots for continuous batching. Values `>= 2` enable batch `run()` and split the KV cache uniformly across slots ([details](./docs/continuous-batching.md)) |
199
+ | cache_checkpoints | 0 – 1024 | 1 | Per-sequence cap on process-local checkpoints kept for cached requests on hybrid / recurrent models and DeepSeek V4, one per committed request at the end of its history. Each holds only the state a tail trim cannot rebuild (about 20 MB on Qwen3.5-0.8B). `1` serves the next turn and a regenerate; editing the user message k-th from the end (1 = the last) without reprocessing the whole conversation needs `k + 1`, so `2` for the last one; `0` disables them. Also accepted as `cache-checkpoints` ([details](./docs/cache-api.md), [how it works](./docs/cache-lifecycle.md)) |
200
+ | cache_checkpoints_max_bytes | integer (bytes) | 0 (unlimited) | Byte budget for those checkpoints, enforced before the count. The load fails with `InvalidArgument` when it cannot hold `cache_checkpoints` checkpoints of the largest size the context allows. Also accepted as `cache-checkpoints-max-bytes` |
201
+ | cache_checkpoint_storage | `"memory"` or `"disk"` | `"memory"` | Where checkpoints and the per-request rollback snapshot live. `"memory"` keeps them in host RAM so a cached chat never touches the disk; `"disk"` writes them to a private directory inside `cache_checkpoint_dir`, which it requires. Also accepted as `cache-checkpoint-storage` |
202
+ | cache_checkpoint_dir | directory path | none | **Required with `cache_checkpoint_storage: "disk"`**, and refused without it: where checkpoint files go, inside a private directory the addon creates in it (mode 0700; on Windows, the directory itself). On mobile, pass the app's cache directory. The load fails with `InvalidArgument` when it is missing, empty, or unusable. Also accepted as `cache-checkpoint-dir` |
203
+ | cache_ram_mib | 0 – 1048576 (MiB) | 0 (off) | Host-RAM budget for conversations that are not running. A `cacheKey` switch or a batch slot eviction moves the conversation there instead of writing its file, and the next request on that key restores it. A write-back cache: unsaved turns reach the file on `saveCache()`, when the budget pushes the entry out, or at unload; ephemeral entries are dropped instead. Also accepted as `cache-ram-mib` ([details](./docs/cache-api.md#keep-switched-away-conversations-in-ram)) |
183
204
  | flash-attn | `"on"`/`"enabled"`/`"true"`/`"1"`, `"off"`/`"disabled"`/`"false"`/`"0"`, or `"auto"` | `"on"`, except when finetuning or on a BitNet model, where it is forced off | Flash attention. The four truthy and four falsey spellings are equivalent; `"auto"` is a third state that defers to qvac-fabric's runtime capability probe. Lower-case only — matching is case-sensitive and any other value is rejected. Affects the KV-cache auto-default — see below. Also accepted as `flash_attn`; supplying both spellings is an error |
184
205
  | cache-type-k | `f16`, `f32`, `bf16`, `q8_0`, `q4_0`, … | auto (see below) | KV-cache **key** quantization type. Unset = auto-default (see KV-cache type below) |
185
206
  | cache-type-v | `f16`, `f32`, `bf16`, `q8_0`, `q4_0`, … | auto (see below) | KV-cache **value** quantization type. Quantizing V requires `flash-attn` on |
@@ -192,7 +213,7 @@ The addon picks a safe KV-cache type when `cache-type-k`/`cache-type-v` are unse
192
213
 
193
214
  - **Auto-default:** on a **Metal / Vulkan GPU** (with flash attention on) both K and V default to **`q8_0`** — quality-neutral vs `f16` and ~47% smaller KV cache. **CPU** and **OpenCL (Adreno)** keep **`f16`** (ARM CPU `q8_0` has a quality/throughput cost; quantized KV is unsafe on OpenCL — see below). Finetuning manages its own KV types and is left untouched.
194
215
  - **`flash-attn: 'auto'` keeps `f16`.** "Flash attention on" above means a truthy `flash-attn` — `on`, `enabled`, `true` or `1`, or the `on` default when the key is unset. `'auto'` is deliberately excluded: quantizing the V cache forces qvac-fabric to promote AUTO to ENABLED, which skips the runtime capability probe that `'auto'` exists to run. So `'auto'` trades the ~47% KV-cache saving for letting qvac-fabric decide. To get both, set `cache-type-k`/`-v` explicitly alongside `'auto'`, or use `'on'`. **`split-mode: 'tensor'` is the exception:** qvac-fabric promotes AUTO to ENABLED unconditionally for that mode, so there is no probe to preserve and `'auto'` takes the q8_0 default there exactly as `'on'` does.
195
- - **OpenCL (Adreno) accepts only `f16`/`f32`/`bf16`:** any other cache type — quantized (`q8_0`, `q4_0`, `q4_1`, `q5_0`, …) or unrecognized — throws a `StatusError`. A quantized K or V cache aborts in `llama_kv_cache::update` on cache management (reasoning-block compaction, state restore) because ggml-opencl has no `F32→quantized` requantize kernel. Use `f16`/`f32`/`bf16`, or a Vulkan GPU / CPU.
216
+ - **OpenCL (Adreno) accepts only `f16`/`f32`/`bf16`:** any other cache type — quantized (`q8_0`, `q4_0`, `q4_1`, `q5_0`, …) or unrecognized — throws a `StatusError`. A quantized K or V cache aborts in `llama_kv_cache::update` during state restore because ggml-opencl has no `F32→quantized` requantize kernel. Use `f16`/`f32`/`bf16`, or a Vulkan GPU / CPU.
196
217
  - **Mixed K≠V is a warning, not an error:** if K and V differ and at least one is quantized, the addon logs a warning (asymmetric quantized K/V falls off the fused flash-attention path — a notable GPU decode penalty — for no quality benefit, and is unsupported on Adreno OpenCL) but proceeds. Prefer a symmetric type. (This may be relaxed once qvac-fabric handles asymmetric quantized K/V efficiently.)
197
218
 
198
219
 
@@ -347,7 +368,7 @@ Admission is controlled by `rejectWhenBusy` (instance-level `opts.rejectWhenBusy
347
368
 
348
369
  #### Prefill (cache warming) with `parallel >= 2`
349
370
 
350
- A prefill-only run (`runOptions.prefill: true`) is admitted on a parallel model only when its product survives the slot teardown, i.e. it is *persistable*: `saveCacheToDisk: true` plus a `cacheKey`. A live-only prefill (no persistence) warms context state that no concurrent job could ever reach, so it is rejected with `InvalidArgument`; run it on a `parallel: 1` model instead. The same rule applies per item in batch runs. See [cache-api.md](./docs/cache-api.md).
371
+ A prefill-only run (`runOptions.prefill: true`) is admitted on a parallel model only when its product survives the slot teardown, i.e. it has a `cacheKey`: the warmed conversation stays in its slot for the next request on that key. A keyless prefill warms context state that no concurrent job could ever reach, so it is rejected with `InvalidArgument`; run it on a `parallel: 1` model instead. The same rule applies per item in batch runs. See [cache-api.md](./docs/cache-api.md).
351
372
 
352
373
  #### Cancelling a batch
353
374
 
package/addon.d.ts CHANGED
@@ -13,7 +13,7 @@ export interface AddonMessage {
13
13
  */
14
14
  generationParams?: GenerationParams;
15
15
  cacheKey?: string;
16
- saveCacheToDisk?: boolean;
16
+ ephemeral?: boolean;
17
17
  }
18
18
  export interface AddonMediaMessage {
19
19
  type: "media";
@@ -74,6 +74,8 @@ export interface LlamaBinding {
74
74
  cancel(handle: unknown, savePauseCheckpoint: number): Promise<void> | void;
75
75
  cancelJob(handle: unknown, id: number): Promise<void> | void;
76
76
  finetune?(handle: unknown, params: FinetuneOptions): Promise<number | false> | number | false;
77
+ saveCache(handle: unknown, cacheKey: string): Promise<void>;
78
+ discardCache(handle: unknown, cacheKey: string): Promise<void>;
77
79
  runJob(handle: unknown, data: AddonRunJobMessage[]): Promise<AddonRunJobResult>;
78
80
  runJob(handle: unknown, data: AddonBatchRunItem[]): Promise<AddonBatchRunResult>;
79
81
  destroyInstance(handle: unknown): void;
@@ -127,6 +129,16 @@ export declare class LlamaInterface {
127
129
  * jobs running. Routes to MultiJobScheduler::cancel(id) -> cancelById(id).
128
130
  */
129
131
  cancelJob(id: number): Promise<void>;
132
+ /**
133
+ * Write the conversation kept in memory for `cacheKey` to its file (see
134
+ * `LlmLlamacpp.saveCache`).
135
+ */
136
+ saveCache(cacheKey: string): Promise<void>;
137
+ /**
138
+ * Drop the conversation kept in memory for `cacheKey` without writing it
139
+ * (see `LlmLlamacpp.discardCache`).
140
+ */
141
+ discardCache(cacheKey: string): Promise<void>;
130
142
  /**
131
143
  * Run finetuning when native binding provides support.
132
144
  */
package/addon.js CHANGED
@@ -160,6 +160,24 @@ class LlamaInterface {
160
160
  }
161
161
  await this._binding.cancelJob(this._handle, id);
162
162
  }
163
+ /**
164
+ * Write the conversation kept in memory for `cacheKey` to its file (see
165
+ * `LlmLlamacpp.saveCache`).
166
+ */
167
+ saveCache(cacheKey) {
168
+ if (!this._handle)
169
+ return Promise.reject(new Error("Model is not loaded"));
170
+ return Promise.resolve(this._binding.saveCache(this._handle, cacheKey));
171
+ }
172
+ /**
173
+ * Drop the conversation kept in memory for `cacheKey` without writing it
174
+ * (see `LlmLlamacpp.discardCache`).
175
+ */
176
+ discardCache(cacheKey) {
177
+ if (!this._handle)
178
+ return Promise.reject(new Error("Model is not loaded"));
179
+ return Promise.resolve(this._binding.discardCache(this._handle, cacheKey));
180
+ }
163
181
  /**
164
182
  * Run finetuning when native binding provides support.
165
183
  */
package/index.d.ts CHANGED
@@ -55,6 +55,21 @@ interface LlmLlamacpp {
55
55
  cancel(): Promise<void>;
56
56
  pause(): Promise<void>;
57
57
  unload(): Promise<void>;
58
+ /**
59
+ * Write the conversation kept in memory for `cacheKey` to that file now.
60
+ * Waits for a request running on that key, not one still queued, so read a
61
+ * response to the end first. Writes only when the file lacks turns, and
62
+ * rejects when nothing is cached under the key or the write fails (the
63
+ * conversation then stays in memory, still unsaved).
64
+ */
65
+ saveCache(cacheKey: string): Promise<void>;
66
+ /**
67
+ * Drop the conversation kept in memory for `cacheKey` without writing it,
68
+ * so no later eviction or unload writes it. Waits for a request running on
69
+ * that key. The file, if one was written, is left alone: delete it too to
70
+ * remove the conversation from disk.
71
+ */
72
+ discardCache(cacheKey: string): Promise<void>;
58
73
  getState(): {
59
74
  configLoaded: boolean;
60
75
  };
@@ -117,6 +132,13 @@ declare namespace LlmLlamacpp {
117
132
  }
118
133
  interface LlamaConfig {
119
134
  device?: string;
135
+ /**
136
+ * Comma-separated GPU backend priority list, e.g. `'cuda,vulkan'`.
137
+ * Accepted names: cuda, vulkan, metal, opencl, plus
138
+ * `auto` for no preference. An unrecognised name is rejected; a recognised
139
+ * one with no device present is skipped. Use `device: 'cpu'` to run on CPU.
140
+ */
141
+ backend?: string;
120
142
  gpu_layers?: NumericLike;
121
143
  ctx_size?: NumericLike;
122
144
  system_prompt?: string;
@@ -264,6 +286,63 @@ declare namespace LlmLlamacpp {
264
286
  * `InvalidArgument` naming both knobs.
265
287
  */
266
288
  parallel?: NumericLike;
289
+ /**
290
+ * Per-sequence cap on the process-local full-state checkpoints kept for
291
+ * cached requests on hybrid / recurrent models (Qwen3.5, Jamba,
292
+ * Granite-Hybrid, DeepSeek V4, ...). Such models cannot trim a KV tail,
293
+ * so a diverging history is served by restoring the longest checkpoint
294
+ * that is still a prefix of the new prompt. A committed cached request
295
+ * adds one, at the end of its history, holding only the state a tail
296
+ * trim cannot rebuild. The default is 1: the last one, which an ordinary
297
+ * next turn and a regenerate restore. `2` also keeps the one before it,
298
+ * which an edit of the last user message restores. Changing the user
299
+ * message k-th from the end (1 = the last) without reprocessing the whole
300
+ * conversation needs at least k + 1; with the default 1, editing the last
301
+ * user message reprocesses everything. `0` keeps and takes none (every
302
+ * divergent turn is a cold prefill), the maximum is 1024.
303
+ * Ignored on pure-attention models, which never take checkpoints.
304
+ * Also accepted as `cache-checkpoints`; supplying both is an error.
305
+ */
306
+ cache_checkpoints?: NumericLike;
307
+ /**
308
+ * Byte budget for the checkpoints kept per sequence, enforced before
309
+ * `cache_checkpoints`: the oldest checkpoints are dropped until the total
310
+ * payload fits. `0` (default) is unlimited. When set, the model load fails
311
+ * with `InvalidArgument` if the budget cannot hold `cache_checkpoints`
312
+ * checkpoints of the largest size the context allows, so a misconfigured
313
+ * budget is reported up front rather than discovered mid-conversation.
314
+ * Also accepted as `cache-checkpoints-max-bytes`.
315
+ */
316
+ cache_checkpoints_max_bytes?: NumericLike;
317
+ /**
318
+ * Where checkpoints and the per-request rollback snapshot live:
319
+ * `'memory'` (default) keeps them in host RAM so a cached chat never
320
+ * touches the disk, `'disk'` writes them to files in
321
+ * `cache_checkpoint_dir`, which it requires. Each snapshot holds only the
322
+ * state a tail trim cannot rebuild, a size fixed by the model (about 20 MB
323
+ * on Qwen3.5-0.8B). Also accepted as `cache-checkpoint-storage`.
324
+ */
325
+ cache_checkpoint_storage?: "disk" | "memory";
326
+ /**
327
+ * Required with `cache_checkpoint_storage: 'disk'`, and refused without
328
+ * it: where checkpoint files go, inside a private directory the addon
329
+ * creates in it (mode 0700; on Windows, the directory itself). There is
330
+ * no default, because the OS temp directory is not usable on every
331
+ * platform (Android); on mobile, pass the app's cache directory. The load
332
+ * fails with `InvalidArgument` when it is missing, empty, or unusable.
333
+ * Also accepted as `cache-checkpoint-dir`.
334
+ */
335
+ cache_checkpoint_dir?: string;
336
+ /**
337
+ * Host-RAM budget, in MiB, for conversations that are not running: a
338
+ * `cacheKey` switch (single prompt) or a slot eviction (batch) moves the
339
+ * conversation there instead of writing its file, and the next request on
340
+ * that key restores it from there. A write-back cache: unsaved turns reach
341
+ * the file on `saveCache()`, when the budget pushes the entry out, or at
342
+ * unload; ephemeral entries are dropped instead. `0` (default) is off; the
343
+ * maximum is 1048576 (1 TiB). Also accepted as `cache-ram-mib`.
344
+ */
345
+ cache_ram_mib?: NumericLike;
267
346
  [key: string]: string | number | boolean | string[] | undefined;
268
347
  }
269
348
  interface LlmLlamacppArgs {
@@ -287,6 +366,14 @@ declare namespace LlmLlamacpp {
287
366
  interface UserTextMessage {
288
367
  role: "system" | "assistant" | "user" | "tool" | "session" | string;
289
368
  content: string;
369
+ /**
370
+ * An assistant turn's reasoning, separate from its answer in `content`, as
371
+ * in OpenAI-compatible APIs. Optional: an answer sent back with its
372
+ * reasoning inline (`<think>…</think>` first, as the model streamed it) is
373
+ * split into the two, using the reasoning markers the chat template
374
+ * reports, so the template can drop or place it. Ignored on other roles.
375
+ */
376
+ reasoning_content?: string;
290
377
  type?: undefined;
291
378
  [key: string]: any;
292
379
  }
@@ -349,6 +436,12 @@ declare namespace LlmLlamacpp {
349
436
  * `"required"` or a function name without tools throws.
350
437
  */
351
438
  tool_choice?: "auto" | "none" | "required" | (string & {});
439
+ /**
440
+ * Whether one response may carry more than one tool call. `true` lets the
441
+ * template and the tool-call grammar accept several; unset or `false`
442
+ * keeps one. Ignored when the prompt carries no tools.
443
+ */
444
+ parallel_tool_calls?: boolean;
352
445
  /**
353
446
  * Per-request reasoning channel budget. `-1` keeps the model's reasoning
354
447
  * channel on; `0` disables it for this request; any positive integer caps
@@ -357,120 +450,37 @@ declare namespace LlmLlamacpp {
357
450
  * value is restored afterwards.
358
451
  */
359
452
  reasoning_budget?: number;
360
- /**
361
- * When the model emits a reasoning block during generation (e.g.
362
- * `<think>...</think>` for the Qwen3 family, `<|channel>thought ...
363
- * <channel|>` for Gemma 4), drop those tokens from the KV cache at
364
- * end-of-generation so subsequent turns do not accumulate reasoning
365
- * history.
366
- *
367
- * Defaults to `false` for all models except the Qwen3 reasoning family
368
- * (Qwen3, Qwen3.5, and Qwen3.6, including MoE variants), which defaults
369
- * to `true`. Set this per-request `generationParams` value to override the
370
- * model default. Set to `false` to preserve reasoning tokens in the KV / SSM
371
- * cache across turns (e.g. chain-of-thought agents that want the next turn
372
- * to attend to prior reasoning, interpretability tooling, or cache-reuse
373
- * patterns that depend on the reasoning-inclusive state). Supported on both
374
- * text and multimodal contexts. No-op for models without a recognised
375
- * reasoning channel.
376
- *
377
- * Every model kind is handled the same way: the sequence is rewound to a
378
- * boundary anchored BEFORE the reasoning span, and the tokens that sit
379
- * outside the span, the pre-reasoning preamble and the answer tail, are
380
- * replayed through the decoder. Only the anchor's form differs. Recurrent
381
- * / hybrid-SSM models (Qwen3.5, Qwen3-Next, Jamba, Granite-Hybrid, ...)
382
- * anchor a full-state snapshot, because the recurrent half cannot be
383
- * rewound by dropping cells; pure-attention models anchor a bare position
384
- * and rewind with a tail trim.
385
- *
386
- * No structural reasoning marker is seeded or replayed, so the compacted
387
- * cache is preamble plus answer with no `<think>` / `</think>` scaffold
388
- * left behind, and close-marker length decides nothing: a marker that
389
- * tokenises to several pieces is supported like any other. Chat templates
390
- * that force-open the reasoning channel during prefill and templates that
391
- * let the model generate the opener are both supported; on the
392
- * generated-opener path the sampled pieces that open the block are clipped
393
- * out of the replay rather than rebuilt.
394
- *
395
- * Prefill-only (cache-warm) requests anchor nothing: they never enter
396
- * generation and cannot emit reasoning tokens.
397
- *
398
- * Uniform hard-fail contract: any inability to remove the reasoning
399
- * span from cache, whether the boundary anchor, the rewind, or the
400
- * replay step, is surfaced to the caller as a `StatusError`. There is no
401
- * soft-failure counter: if the feature is
402
- * enabled and cache cleanup cannot complete, the final request result is
403
- * failed rather than reported as a successful answer with the reasoning span
404
- * still resident in cache.
405
- *
406
- * Streaming caveat: token callbacks (`outputCallback` / batch `onToken`) are
407
- * invoked during generation, while reasoning-block compaction runs at
408
- * end-of-generation. If compaction fails, streaming callers may already have
409
- * received partial or complete text. Treat streamed text as tentative until
410
- * the request completes successfully; non-streaming callers receive no
411
- * successful returned answer on this failure path.
412
- *
413
- * Before throwing, the affected sequence is cleaned up so that the
414
- * next request on the same context starts from a coherent state:
415
- * * Boundary-anchor failure: nothing has been rewound yet, so the
416
- * driver rolls back to its pre-prompt checkpoint (or clears the
417
- * sequence entirely on restore underflow) and resets positional
418
- * accounting, then rethrows.
419
- * * Rewind or replay failure: compaction rewinds before it replays,
420
- * so live KV has already been written to by this point and a tail
421
- * trim can no longer reach a coherent state. The compactor
422
- * best-effort wipes the sequence and the driver zeroes its
423
- * positional accounting to match, so subsequent turns cannot decode
424
- * into contaminated positions.
425
- *
426
- * On the continuous-batch path, the scheduler's error-recovery leg
427
- * deliberately does NOT persist the failed slot's cache: when the
428
- * request was configured with `cacheKey` + `saveCacheToDisk`, the
429
- * last known-good on-disk cache is preserved rather than being
430
- * overwritten with the post-failure state. The same skip-save rule
431
- * applies to graceful cancels of hybrid / recurrent requests when
432
- * rollback to the pre-request cursor cannot be completed (recurrent
433
- * full-state restore refused, or no pre-request snapshot was captured
434
- * yet the driver has advanced past the pre-request cursor). Cancels
435
- * that can be rolled back cleanly still persist as usual.
436
- */
437
- remove_thinking_from_context?: boolean;
438
453
  }
439
454
  interface RunOptions {
440
455
  /**
441
456
  * Run prefill only (cache warming): the prompt is evaluated but no tokens
442
- * are generated. On a model loaded with `parallel >= 2` a prefill is
443
- * admitted only when it is *persistable* (`saveCacheToDisk: true` plus a
444
- * `cacheKey`) — a live-only prefill warms context state that no concurrent
445
- * job could reach and is rejected with `InvalidArgument`; run live-only
446
- * prefills on a `parallel: 1` model. The same rule applies per batch item.
457
+ * are generated. On a model loaded with `parallel >= 2` a prefill needs a
458
+ * `cacheKey` (the warmed conversation stays in its slot for the next
459
+ * request on that key) — a keyless prefill warms context state that no
460
+ * concurrent job could reach and is rejected with `InvalidArgument`; run
461
+ * keyless prefills on a `parallel: 1` model. The same rule applies per
462
+ * batch item.
447
463
  */
448
464
  prefill?: boolean;
449
465
  generationParams?: GenerationParams;
466
+ /**
467
+ * Enables addon-owned prompt caching at this path. Every cached request
468
+ * must resend the complete authoritative message history and tool list;
469
+ * delta-only continuations are not supported. The addon renders once and
470
+ * decodes only the suffix after the longest matching token/media prefix.
471
+ */
450
472
  cacheKey?: string;
451
473
  /**
452
- * When `true` and `cacheKey` is set, the driver persists the sequence's
453
- * KV / recurrent state to disk under `cacheKey` at end-of-generation so a
454
- * later run keyed by the same string can resume without re-prefilling.
455
- *
456
- * The continuous-batch scheduler intentionally SKIPS the save on
457
- * teardown legs where persistence could corrupt the last known-good
458
- * on-disk cache:
459
- * - Any batch error-recovery path (e.g. decode failure, per-slot
460
- * failure with `SaveCachePolicy::Skip`, or a
461
- * `remove_thinking_from_context` hard-fail).
462
- * - Graceful cancel of a hybrid / recurrent request whose driver
463
- * cannot roll live memory back to the pre-request cursor —
464
- * either the recurrent full-state restore was refused, or no
465
- * pre-request snapshot exists yet the driver advanced past the
466
- * pre-request cursor. Cancels that roll back cleanly still save.
467
- *
468
- * On both skip paths the sequence's in-memory KV is still cleared, so
469
- * subsequent requests decode from a coherent baseline; only the
470
- * on-disk cache is untouched. Pure-attention drivers always roll back
471
- * via `removeLastNTokens` and therefore save on cancel as usual.
474
+ * Keep this conversation in memory only. By default a cached
475
+ * conversation is written to its `cacheKey` file whenever it would
476
+ * otherwise be lost with turns the file lacks: when it is set aside for
477
+ * another key, evicted from its batch slot or from the RAM tier, or the
478
+ * model is reloaded or unloaded. With `ephemeral: true` each of those
479
+ * drops it instead, so its next request starts cold. It still loads an
480
+ * existing file, and an explicit `saveCache(cacheKey)` still writes it.
481
+ * The flag belongs to the conversation's latest request.
472
482
  */
473
- saveCacheToDisk?: boolean;
483
+ ephemeral?: boolean;
474
484
  /**
475
485
  * Admission policy when the worker pool is full. `true` rejects before
476
486
  * submitting with an `Error` carrying `code === 'RUN_BUSY'` — branch on the
@@ -523,15 +533,6 @@ declare namespace LlmLlamacpp {
523
533
  CacheTokens: number;
524
534
  generatedTokens: number;
525
535
  promptTokens: number;
526
- /**
527
- * Number of `<think>` (or model-equivalent) reasoning blocks dropped
528
- * from the KV cache at end-of-generation by the
529
- * `remove_thinking_from_context` feature. Per-inference for single
530
- * requests; summed across completed slots for batch requests. 0 when
531
- * the model has no recognised reasoning channel, when the feature
532
- * was disabled per-request, or when no reasoning blocks were emitted.
533
- */
534
- thinkingBlockDiscards: number;
535
536
  /**
536
537
  * Number of prompt renders in this request that provably left the tool
537
538
  * definitions out — the template either rejected them, or supplying them
package/index.js CHANGED
@@ -15,7 +15,7 @@ function normalizeRunOptions(runOptions) {
15
15
  prefill: false,
16
16
  generationParams: undefined,
17
17
  cacheKey: undefined,
18
- saveCacheToDisk: false,
18
+ ephemeral: false,
19
19
  rejectWhenBusy: undefined,
20
20
  };
21
21
  }
@@ -35,8 +35,13 @@ function normalizeRunOptions(runOptions) {
35
35
  if (options.cacheKey !== undefined && typeof options.cacheKey !== "string") {
36
36
  throw new TypeError("cacheKey must be a string when provided");
37
37
  }
38
- if (options.saveCacheToDisk !== undefined && typeof options.saveCacheToDisk !== "boolean") {
39
- throw new TypeError("saveCacheToDisk must be a boolean when provided");
38
+ if (options.saveCacheToDisk !== undefined) {
39
+ throw new TypeError("saveCacheToDisk was removed: a cached conversation stays in memory and is written to " +
40
+ "its cacheKey file when it is set aside, evicted or the model unloads; call " +
41
+ "saveCache(cacheKey) to write it now, or set ephemeral: true to never write it");
42
+ }
43
+ if (options.ephemeral !== undefined && typeof options.ephemeral !== "boolean") {
44
+ throw new TypeError("ephemeral must be a boolean when provided");
40
45
  }
41
46
  if (options.rejectWhenBusy !== undefined && typeof options.rejectWhenBusy !== "boolean") {
42
47
  throw new TypeError("rejectWhenBusy must be a boolean when provided");
@@ -45,7 +50,7 @@ function normalizeRunOptions(runOptions) {
45
50
  prefill: options.prefill === true,
46
51
  generationParams: normalizeGenerationParams(options.generationParams),
47
52
  cacheKey: options.cacheKey,
48
- saveCacheToDisk: options.saveCacheToDisk === true,
53
+ ephemeral: options.ephemeral === true,
49
54
  // Left undefined when unset so admission falls back to the instance default.
50
55
  rejectWhenBusy: options.rejectWhenBusy,
51
56
  };
@@ -54,7 +59,7 @@ function promptToAddonMessages(prompt, runOptions) {
54
59
  if (!Array.isArray(prompt)) {
55
60
  throw new TypeError("Prompt input must be Message[]");
56
61
  }
57
- const { prefill, generationParams, cacheKey, saveCacheToDisk } = normalizeRunOptions(runOptions);
62
+ const { prefill, generationParams, cacheKey, ephemeral } = normalizeRunOptions(runOptions);
58
63
  const textMessages = [];
59
64
  const mediaItems = [];
60
65
  for (const message of prompt) {
@@ -77,7 +82,7 @@ function promptToAddonMessages(prompt, runOptions) {
77
82
  prefill,
78
83
  generationParams,
79
84
  cacheKey,
80
- saveCacheToDisk,
85
+ ephemeral,
81
86
  });
82
87
  return promptMessages;
83
88
  }
@@ -97,8 +102,8 @@ const GENERATION_PARAM_KEYS = new Set([
97
102
  "grammar",
98
103
  "json_schema",
99
104
  "tool_choice",
105
+ "parallel_tool_calls",
100
106
  "reasoning_budget",
101
- "remove_thinking_from_context",
102
107
  ]);
103
108
  // Normalizes the per-request `generationParams.json_schema` field. The
104
109
  // addon binding expects a string; callers commonly pass a plain object
@@ -137,14 +142,14 @@ function normalizeGenerationParams(generationParams) {
137
142
  params[key] = value;
138
143
  }
139
144
  const sanitized = params;
140
- if (sanitized.remove_thinking_from_context !== undefined &&
141
- typeof sanitized.remove_thinking_from_context !== "boolean") {
142
- throw new TypeError("generationParams.remove_thinking_from_context must be a boolean when provided");
143
- }
144
145
  if (sanitized.tool_choice !== undefined &&
145
146
  (typeof sanitized.tool_choice !== "string" || sanitized.tool_choice.length === 0)) {
146
147
  throw new TypeError('generationParams.tool_choice must be "auto", "none", "required" or a declared function name');
147
148
  }
149
+ if (sanitized.parallel_tool_calls !== undefined &&
150
+ typeof sanitized.parallel_tool_calls !== "boolean") {
151
+ throw new TypeError("generationParams.parallel_tool_calls must be a boolean when provided");
152
+ }
148
153
  const hasGrammar = typeof sanitized.grammar === "string" && sanitized.grammar.length > 0;
149
154
  const hasJsonSchema = sanitized.json_schema !== undefined &&
150
155
  sanitized.json_schema !== null &&
@@ -781,6 +786,46 @@ const LlmLlamacpp = class LlmLlamacpp {
781
786
  get _hasActiveResponse() {
782
787
  return (this.addon ? this.addon.activeJobs() : 0) > 0;
783
788
  }
789
+ /**
790
+ * Write the conversation kept in memory for `cacheKey` to that file now:
791
+ * the active single-prompt conversation, a batch conversation kept in its
792
+ * slot, or one in the RAM tier. Resolves once the file is written. When a
793
+ * request on that key is running it waits for it to finish, so the file
794
+ * always holds a committed state. A request still queued is not waited
795
+ * for: `run()` resolves when the request is accepted, so read its response
796
+ * to the end before saving when the file must hold that turn. Resolves without writing when the file
797
+ * already holds the conversation. Rejects when nothing is cached under the
798
+ * key and no file exists, or when the write fails (the conversation then
799
+ * stays in memory, still unsaved, so the call can be retried). Writes
800
+ * ephemeral conversations too.
801
+ */
802
+ async saveCache(cacheKey) {
803
+ if (typeof cacheKey !== "string" || cacheKey.length === 0) {
804
+ throw new TypeError("saveCache(cacheKey) requires a non-empty string");
805
+ }
806
+ if (!this.addon) {
807
+ throw new Error("Model is not loaded");
808
+ }
809
+ await this.addon.saveCache(cacheKey);
810
+ }
811
+ /**
812
+ * Drop the conversation kept in memory for `cacheKey` — the active
813
+ * single-prompt conversation, a batch conversation kept in its slot, its
814
+ * RAM-tier entry and its checkpoints — without writing it, so no later
815
+ * eviction or unload writes it. When a request on that key is running it
816
+ * waits for it to finish. The file, if one was written, is left alone:
817
+ * delete it as well to remove the conversation from disk. Resolves when
818
+ * nothing is kept for the key.
819
+ */
820
+ async discardCache(cacheKey) {
821
+ if (typeof cacheKey !== "string" || cacheKey.length === 0) {
822
+ throw new TypeError("discardCache(cacheKey) requires a non-empty string");
823
+ }
824
+ if (!this.addon) {
825
+ throw new Error("Model is not loaded");
826
+ }
827
+ await this.addon.discardCache(cacheKey);
828
+ }
784
829
  getState() {
785
830
  return this.state;
786
831
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/llm-llamacpp",
3
- "version": "0.56.0",
3
+ "version": "0.58.0",
4
4
  "description": "llama addon for qvac",
5
5
  "addon": true,
6
6
  "scripts": {
@@ -78,7 +78,7 @@
78
78
  "@types/node": "^24.2.1",
79
79
  "bare-crypto": "^1.15.3",
80
80
  "bare-process": "^4.2.2",
81
- "bare-subprocess": "^5.2.3",
81
+ "bare-subprocess": "^6.1.0",
82
82
  "bare-url": "^2.1.6",
83
83
  "brittle": "^3.16.5",
84
84
  "cmake-bare": "^1.7.5",
@@ -96,7 +96,7 @@
96
96
  "typescript-eslint": "^8.63.0"
97
97
  },
98
98
  "dependencies": {
99
- "@qvac/fabric": "^0.20.0",
99
+ "@qvac/fabric": "^0.20.2",
100
100
  "@qvac/infer-base": "^0.6.2",
101
101
  "@qvac/logging": "^0.1.0",
102
102
  "bare-fs": "^4.5.1",