@qvac/llm-llamacpp 0.66.0 → 0.68.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 +38 -36
- package/addon.d.ts +1 -7
- package/addon.js +0 -3
- package/index.d.ts +62 -19
- package/index.js +81 -42
- package/package.json +11 -11
- package/prebuilds/android-arm64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/darwin-arm64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/darwin-arm64/qvac__llm-llamacpp.bare.exports +57 -46
- package/prebuilds/darwin-x64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/darwin-x64/qvac__llm-llamacpp.bare.exports +78 -56
- package/prebuilds/ios-arm64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/ios-arm64/qvac__llm-llamacpp.bare.exports +57 -46
- package/prebuilds/ios-arm64-simulator/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/ios-arm64-simulator/qvac__llm-llamacpp.bare.exports +57 -46
- package/prebuilds/ios-x64-simulator/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/ios-x64-simulator/qvac__llm-llamacpp.bare.exports +78 -56
- package/prebuilds/linux-arm64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/linux-x64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/win32-x64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/win32-x64/qvac__llm-llamacpp.bare.exports +0 -0
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ This native C++ addon, built using the `Bare` Runtime, simplifies running Large
|
|
|
15
15
|
- [5. Load Model](#5-load-model)
|
|
16
16
|
- [6. Run Inference](#6-run-inference)
|
|
17
17
|
- [Tool calling](#tool-calling)
|
|
18
|
+
- [Ordered text and media](#ordered-text-and-media)
|
|
18
19
|
- [Batch inference](#batch-inference)
|
|
19
20
|
- [7. Release Resources](#7-release-resources)
|
|
20
21
|
- [API behavior by state](#api-behavior-by-state)
|
|
@@ -113,7 +114,7 @@ const args = {
|
|
|
113
114
|
|
|
114
115
|
The `args` obj contains the following properties:
|
|
115
116
|
|
|
116
|
-
* `files.model`:
|
|
117
|
+
* `files.model`: Exactly one absolute disk path: the single GGUF file or only the first shard. Fabric reads the remaining shards from the same directory.
|
|
117
118
|
* `files.projectionModel`: Optional. Absolute path to the projection model file. This is required for multimodal support.
|
|
118
119
|
* `config`: The model configuration object (see next section).
|
|
119
120
|
* `logger`: This property is used to create a [`QvacLogger`](../logging) instance, which handles all logging functionality.
|
|
@@ -121,46 +122,24 @@ The `args` obj contains the following properties:
|
|
|
121
122
|
|
|
122
123
|
#### Sharded models
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
Models load from disk only. Pass exactly one absolute path in `files.model`:
|
|
126
|
+
the single GGUF file, or the first shard (`*-00001-of-000NN.gguf`). All shards
|
|
127
|
+
must already be in the same directory with their original names. Fabric reads
|
|
128
|
+
the GGUF metadata, discovers the remaining shards and reads their weights.
|
|
129
|
+
No JavaScript weight buffers or `.tensors.txt` file are needed.
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Example — loading a 5-shard model:
|
|
131
|
+
Passing multiple paths or weight buffers is rejected. Migrate an old all-files
|
|
132
|
+
list to `[firstShardPath]`. The `loadWeights()` API has been removed.
|
|
133
|
+
This removes model-weight streaming, while generated output still streams as before.
|
|
131
134
|
|
|
132
135
|
```js
|
|
133
|
-
const path = require('bare-path')
|
|
134
|
-
const LlmLlamacpp = require('@qvac/llm-llamacpp')
|
|
135
|
-
|
|
136
|
-
const dir = path.resolve('./models')
|
|
137
|
-
const modelBase = 'my-big-model-Q4_K_M'
|
|
138
|
-
|
|
139
136
|
const model = new LlmLlamacpp({
|
|
140
|
-
files: {
|
|
141
|
-
|
|
142
|
-
path.join(dir, `${modelBase}.tensors.txt`),
|
|
143
|
-
path.join(dir, `${modelBase}-00001-of-00005.gguf`),
|
|
144
|
-
path.join(dir, `${modelBase}-00002-of-00005.gguf`),
|
|
145
|
-
path.join(dir, `${modelBase}-00003-of-00005.gguf`),
|
|
146
|
-
path.join(dir, `${modelBase}-00004-of-00005.gguf`),
|
|
147
|
-
path.join(dir, `${modelBase}-00005-of-00005.gguf`)
|
|
148
|
-
]
|
|
149
|
-
},
|
|
150
|
-
config,
|
|
151
|
-
logger: console,
|
|
152
|
-
opts: { stats: true }
|
|
137
|
+
files: { model: ['/absolute/models/model-00001-of-00003.gguf'] },
|
|
138
|
+
config
|
|
153
139
|
})
|
|
154
|
-
|
|
155
140
|
await model.load()
|
|
156
141
|
```
|
|
157
142
|
|
|
158
|
-
For single-file GGUF models, pass a one-element array:
|
|
159
|
-
|
|
160
|
-
```js
|
|
161
|
-
files: { model: [path.join(dir, 'Llama-3.2-1B-Instruct-Q4_0.gguf')] }
|
|
162
|
-
```
|
|
163
|
-
|
|
164
143
|
### 3. Create the `config` obj
|
|
165
144
|
|
|
166
145
|
The `config` obj consists of a set of hyper-parameters which can be used to tweak the behaviour of the model.
|
|
@@ -206,7 +185,8 @@ const config = {
|
|
|
206
185
|
| 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 (on a sliding-window model, a full window). Also accepted as `cache-checkpoints-max-bytes` |
|
|
207
186
|
| 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` |
|
|
208
187
|
| 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` |
|
|
209
|
-
| 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)) |
|
|
188
|
+
| 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; unless `cache_share_across_keys` is `"false"`, a request on another key that shares its prompt prefix starts from a copy of 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)) |
|
|
189
|
+
| cache_share_across_keys | `"true"`/`"on"`/`"1"`, `"false"`/`"off"`/`"0"` | `"true"` | Whether a request on a decoder-only model may start from state another conversation left (keyless memory, another key's state, a `cacheKey` file the model used). `"false"` keeps every request to its own key's state, for callers who must not learn through `cacheTokens` or timing whether another conversation shares their prompt. Also accepted as `cache-share-across-keys` ([details](./docs/cache-api.md#keep-conversations-apart)) |
|
|
210
190
|
| 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 |
|
|
211
191
|
| 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) |
|
|
212
192
|
| cache-type-v | `f16`, `f32`, `bf16`, `q8_0`, `q4_0`, … | auto (see below) | KV-cache **value** quantization type. Quantizing V requires `flash-attn` on |
|
|
@@ -345,6 +325,28 @@ const response = await model.run([
|
|
|
345
325
|
|
|
346
326
|
The tool grammar holds the model to the order of `parameters`. Inside an object it accepts the required properties first, in declared order, then the optional ones, also in declared order. When the model writes a property out of that order, the call isn't rejected with an error: the model drops a field, writes a broken value or never finishes the call. Declare `required` properties before optional ones at every level, and declare each group in the order the model is likely to write it. This applies to nested objects, and to every argument in formats that write the arguments as one JSON object (Qwen3). Qwen3.5's top-level `<parameter=...>` arguments are less strict: up to six required arguments in any order, then the optional ones in any order. An optional argument before a required one is still rejected there.
|
|
347
327
|
|
|
328
|
+
#### Ordered text and media
|
|
329
|
+
|
|
330
|
+
A user message's `content` is either a string or an ordered array of parts. Use the array when the position of an image inside the text matters — multiple-choice options that are pictures, illustrated forms, steps — so the model sees the text and every image in one turn, in order:
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
const response = await model.run([{
|
|
334
|
+
role: 'user',
|
|
335
|
+
content: [
|
|
336
|
+
{ type: 'text', text: 'Which image shows the same place as ' },
|
|
337
|
+
{ type: 'media', content: '/images/place.jpg' },
|
|
338
|
+
{ type: 'text', text: '?\nA. ' },
|
|
339
|
+
{ type: 'media', content: imageABytes }, // raw bytes work too
|
|
340
|
+
{ type: 'text', text: '\nB. ' },
|
|
341
|
+
{ type: 'media', content: '/images/option-b.jpg' }
|
|
342
|
+
]
|
|
343
|
+
}])
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Each media part becomes an `<__media__>` marker at its own position in the rendered turn — the same prompt llama-server builds from an OpenAI content-parts request. A part's `content` is an absolute path or a `Uint8Array`; media parts are only valid on user messages, and an unknown part type is rejected with `InvalidInputFormat`.
|
|
347
|
+
|
|
348
|
+
The alternative — one `{ role: 'user', type: 'media' }` message per part — keeps the order but renders each text part as its own user turn, so text that refers to "this image" ends up in a different turn from the image.
|
|
349
|
+
|
|
348
350
|
#### Batch inference
|
|
349
351
|
|
|
350
352
|
Load the model with `parallel >= 2`, then pass an array of prompts. Each chunk arrives tagged with the id of the sequence that produced it; `await()` returns results in input order.
|
|
@@ -443,7 +445,7 @@ Admission is controlled by `rejectWhenBusy` (instance-level `opts.rejectWhenBusy
|
|
|
443
445
|
|
|
444
446
|
#### Prefill (cache warming) with `parallel >= 2`
|
|
445
447
|
|
|
446
|
-
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
|
|
448
|
+
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 state no later request can count on (at most parked anonymously, first to be evicted), 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).
|
|
447
449
|
|
|
448
450
|
#### Cancelling a batch
|
|
449
451
|
|
|
@@ -735,7 +737,7 @@ npm run test:cpp:models:ci # exactly what .github/workflows/cpp-tests-llm.yml
|
|
|
735
737
|
# downloads; matches what CI exercises
|
|
736
738
|
```
|
|
737
739
|
|
|
738
|
-
First-run downloads pull several GB from Hugging Face. Every fixture is SHA256-verified against a digest pinned in `scripts/download-unit-test-models.js`; mismatched or partial files are re-downloaded automatically. Set `HF_TOKEN` if a repo requires authentication. Override paths with env vars such as `SHARDED_MODEL_FIRST_SHARD_PATH` (see `test/unit/test_common.hpp`).
|
|
740
|
+
First-run downloads pull several GB, mostly from Hugging Face. Every fixture is SHA256-verified against a digest pinned in `scripts/download-unit-test-models.js`; mismatched or partial files are re-downloaded automatically. Set `HF_TOKEN` if a repo requires authentication. Fixtures pinned by `s3Path` (currently `bitnet_b1_58-large-TQ2_0.gguf`) are copied from the QVAC model registry bucket and need `MODEL_S3_BUCKET` plus AWS credentials; if either is missing or the copy fails, a local run skips the fixture with a warning and its tests skip, while `--ci` fails. Override paths with env vars such as `SHARDED_MODEL_FIRST_SHARD_PATH` or `BITNET_MODEL_PATH` (see `test/unit/test_common.hpp`).
|
|
739
741
|
|
|
740
742
|
### Fuzzing
|
|
741
743
|
|
package/addon.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface AddonMessage {
|
|
|
14
14
|
generationParams?: GenerationParams;
|
|
15
15
|
cacheKey?: string;
|
|
16
16
|
ephemeral?: boolean;
|
|
17
|
+
baseCacheKey?: string;
|
|
17
18
|
}
|
|
18
19
|
export interface AddonMediaMessage {
|
|
19
20
|
type: "media";
|
|
@@ -54,11 +55,6 @@ export type AddonBatchRunResult = {
|
|
|
54
55
|
id?: never;
|
|
55
56
|
ids: string[];
|
|
56
57
|
};
|
|
57
|
-
export interface LoadWeightsData {
|
|
58
|
-
filename: string;
|
|
59
|
-
chunk: Uint8Array | null;
|
|
60
|
-
completed: boolean;
|
|
61
|
-
}
|
|
62
58
|
export interface AddonConfigurationParams {
|
|
63
59
|
path: string;
|
|
64
60
|
projectionPath: string;
|
|
@@ -73,7 +69,6 @@ export interface AddonCalcTokensParams {
|
|
|
73
69
|
}
|
|
74
70
|
export interface LlamaBinding {
|
|
75
71
|
createInstance(owner: LlamaInterface, configurationParams: AddonConfigurationParams, outputCallback: AddonOutputCallback, reserved: null): object;
|
|
76
|
-
loadWeights(handle: unknown, data: LoadWeightsData): Promise<void> | void;
|
|
77
72
|
activate(handle: unknown): Promise<void> | void;
|
|
78
73
|
activeJobs(handle: unknown): number;
|
|
79
74
|
activeSlots(handle: unknown): number;
|
|
@@ -104,7 +99,6 @@ export declare class LlamaInterface {
|
|
|
104
99
|
private readonly _binding;
|
|
105
100
|
private _handle;
|
|
106
101
|
constructor(binding: unknown, configurationParams: AddonConfigurationParams, outputCb: AddonOutputCallback);
|
|
107
|
-
loadWeights(weightsData: LoadWeightsData): Promise<void>;
|
|
108
102
|
/**
|
|
109
103
|
* Moves addon to the LISTENING state after all the initialization is done
|
|
110
104
|
*/
|
package/addon.js
CHANGED
|
@@ -80,9 +80,6 @@ class LlamaInterface {
|
|
|
80
80
|
}
|
|
81
81
|
this._handle = this._binding.createInstance(this, configurationParams, outputCb, null);
|
|
82
82
|
}
|
|
83
|
-
loadWeights(weightsData) {
|
|
84
|
-
return Promise.resolve(this._binding.loadWeights(this._handle, weightsData));
|
|
85
|
-
}
|
|
86
83
|
/**
|
|
87
84
|
* Moves addon to the LISTENING state after all the initialization is done
|
|
88
85
|
*/
|
package/index.d.ts
CHANGED
|
@@ -16,11 +16,10 @@ type LlmLlamacppArgs = LlmLlamacpp.LlmLlamacppArgs;
|
|
|
16
16
|
type Message = LlmLlamacpp.Message;
|
|
17
17
|
type RunOptions = LlmLlamacpp.RunOptions;
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* in `GGUFShards::expandGGUFIntoShards`.
|
|
19
|
+
* Validates the single disk path. For a sharded GGUF, pass only the first
|
|
20
|
+
* shard; Fabric discovers the remaining shards. Weight streaming is unsupported.
|
|
22
21
|
*/
|
|
23
|
-
declare function
|
|
22
|
+
declare function validateModelPath(files: string[]): string;
|
|
24
23
|
interface LlmLlamacpp {
|
|
25
24
|
/** The native addon, or `null` before `load()` and after `unload()`. Advanced/test access only. */
|
|
26
25
|
addon: LlmLlamacpp.Addon | null;
|
|
@@ -93,8 +92,8 @@ interface LlmLlamacpp {
|
|
|
93
92
|
interface LlmLlamacppConstructor {
|
|
94
93
|
new (args: LlmLlamacppArgs): LlmLlamacpp;
|
|
95
94
|
readonly prototype: LlmLlamacpp;
|
|
96
|
-
/**
|
|
97
|
-
readonly
|
|
95
|
+
/** Validates and returns the single absolute disk path; split models require the first shard. */
|
|
96
|
+
readonly validateModelPath: typeof validateModelPath;
|
|
98
97
|
readonly assessFit: typeof assessFitImpl;
|
|
99
98
|
}
|
|
100
99
|
/** LLM client wrapping the native LlamaInterface for inference, finetuning, and pause/resume. */
|
|
@@ -119,11 +118,6 @@ declare namespace LlmLlamacpp {
|
|
|
119
118
|
*/
|
|
120
119
|
type AddonBatchRunResult = AddonModule.AddonBatchRunResult;
|
|
121
120
|
interface Addon {
|
|
122
|
-
loadWeights(data: {
|
|
123
|
-
filename: string;
|
|
124
|
-
chunk: Uint8Array | null;
|
|
125
|
-
completed: boolean;
|
|
126
|
-
}, logger?: QvacLogger): Promise<void>;
|
|
127
121
|
activate(): Promise<void>;
|
|
128
122
|
/** Single-request admission: resolves the accepted flag plus the native-assigned job id. */
|
|
129
123
|
runJob(data: AddonRunJobMessage[]): Promise<AddonRunJobResult>;
|
|
@@ -380,6 +374,19 @@ declare namespace LlmLlamacpp {
|
|
|
380
374
|
* maximum is 1048576 (1 TiB). Also accepted as `cache-ram-mib`.
|
|
381
375
|
*/
|
|
382
376
|
cache_ram_mib?: NumericLike;
|
|
377
|
+
/**
|
|
378
|
+
* Whether a request on a decoder-only model may start from state another
|
|
379
|
+
* conversation left: the memory a request without a `cacheKey` keeps,
|
|
380
|
+
* another key's state in a sequence or the RAM tier, or a `cacheKey` file
|
|
381
|
+
* the model has used. `'true'` (default) shares it, as llama-server's
|
|
382
|
+
* prompt cache does. `'false'` keeps every request to its own key's state,
|
|
383
|
+
* for a model serving callers who must not learn through `cacheTokens` or
|
|
384
|
+
* timing whether another conversation shares their prompt: a request
|
|
385
|
+
* without a key starts cold and is reset afterwards, a new key starts cold,
|
|
386
|
+
* and `baseCacheKey` has no effect. Also accepted as
|
|
387
|
+
* `cache-share-across-keys`.
|
|
388
|
+
*/
|
|
389
|
+
cache_share_across_keys?: "true" | "false" | "on" | "off" | "1" | "0";
|
|
383
390
|
/**
|
|
384
391
|
* Speculative decoding, as llama-server's `--spec-type`. `'draft-mtp'`
|
|
385
392
|
* drafts with the model's own multi-token-prediction (MTP / NextN) head,
|
|
@@ -420,8 +427,9 @@ declare namespace LlmLlamacpp {
|
|
|
420
427
|
[key: string]: string | number | boolean | string[] | undefined;
|
|
421
428
|
}
|
|
422
429
|
interface LlmLlamacppArgs {
|
|
430
|
+
/** One absolute disk path, or only the first shard path. Weight buffers, multiple paths and tensor-name files are unsupported. */
|
|
423
431
|
files: {
|
|
424
|
-
model: string
|
|
432
|
+
model: [string];
|
|
425
433
|
projectionModel?: string;
|
|
426
434
|
};
|
|
427
435
|
config: LlamaConfig;
|
|
@@ -437,9 +445,30 @@ declare namespace LlmLlamacpp {
|
|
|
437
445
|
rejectWhenBusy?: boolean;
|
|
438
446
|
};
|
|
439
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* One ordered piece of a message's `content`. Media parts keep their place in
|
|
450
|
+
* the turn: a single user message can interleave text and media, which a
|
|
451
|
+
* string `content` plus separate `type: "media"` messages cannot express
|
|
452
|
+
* without splitting the turn.
|
|
453
|
+
*/
|
|
454
|
+
type UserContentPart = {
|
|
455
|
+
type: "text";
|
|
456
|
+
text: string;
|
|
457
|
+
} | {
|
|
458
|
+
type: "media";
|
|
459
|
+
/**
|
|
460
|
+
* Either an absolute path to the media file (`string`) or its raw bytes
|
|
461
|
+
* (`Uint8Array`), the same two forms a `type: "media"` message accepts.
|
|
462
|
+
*/
|
|
463
|
+
content: string | Uint8Array;
|
|
464
|
+
};
|
|
440
465
|
interface UserTextMessage {
|
|
441
466
|
role: "system" | "assistant" | "user" | "tool" | "session" | string;
|
|
442
|
-
|
|
467
|
+
/**
|
|
468
|
+
* Plain text, or ordered text/media parts rendered as one turn. Media parts
|
|
469
|
+
* are only valid on user messages.
|
|
470
|
+
*/
|
|
471
|
+
content: string | UserContentPart[];
|
|
443
472
|
/**
|
|
444
473
|
* An assistant turn's reasoning, separate from its answer in `content`, as
|
|
445
474
|
* in OpenAI-compatible APIs. Optional: an answer sent back with its
|
|
@@ -554,10 +583,9 @@ declare namespace LlmLlamacpp {
|
|
|
554
583
|
* Run prefill only (cache warming): the prompt is evaluated but no tokens
|
|
555
584
|
* are generated. On a model loaded with `parallel >= 2` a prefill needs a
|
|
556
585
|
* `cacheKey` (the warmed conversation stays in its slot for the next
|
|
557
|
-
* request on that key) — a keyless prefill warms
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
* batch item.
|
|
586
|
+
* request on that key) — a keyless prefill warms state no later request
|
|
587
|
+
* can count on and is rejected with `InvalidArgument`; run keyless
|
|
588
|
+
* prefills on a `parallel: 1` model. The same rule applies per batch item.
|
|
561
589
|
*/
|
|
562
590
|
prefill?: boolean;
|
|
563
591
|
generationParams?: GenerationParams;
|
|
@@ -574,11 +602,26 @@ declare namespace LlmLlamacpp {
|
|
|
574
602
|
* otherwise be lost with turns the file lacks: when it is set aside for
|
|
575
603
|
* another key, evicted from its batch slot or from the RAM tier, or the
|
|
576
604
|
* model is reloaded or unloaded. With `ephemeral: true` each of those
|
|
577
|
-
* drops it instead, so its next request
|
|
578
|
-
*
|
|
605
|
+
* drops it instead, so its next request finds no state of its own (it
|
|
606
|
+
* still reuses any prefix it shares with what the model holds, except on
|
|
607
|
+
* encoder-decoder models). It still loads an existing file, and an
|
|
608
|
+
* explicit `saveCache(cacheKey)` still writes it.
|
|
579
609
|
* The flag belongs to the conversation's latest request.
|
|
580
610
|
*/
|
|
581
611
|
ephemeral?: boolean;
|
|
612
|
+
/**
|
|
613
|
+
* A `cacheKey` file to start from when nothing in memory serves more of
|
|
614
|
+
* the prompt, such as a system prompt and tools primed with `prefill`.
|
|
615
|
+
* It is read, never written: the request's own state goes to its
|
|
616
|
+
* `cacheKey`. A file that is missing or shares no prefix with the prompt
|
|
617
|
+
* is skipped. Once named, the file stays a source for later requests on
|
|
618
|
+
* this model, as any `cacheKey` file it used does, until files named
|
|
619
|
+
* or picked as a prompt's closest match more recently push it out (the
|
|
620
|
+
* model keeps the 256 most recent, as far as 64 MiB of memory holds
|
|
621
|
+
* their prompt records). No effect with `cache_share_across_keys:
|
|
622
|
+
* 'false'`.
|
|
623
|
+
*/
|
|
624
|
+
baseCacheKey?: string;
|
|
582
625
|
/**
|
|
583
626
|
* Admission policy when the worker pool is full. `true` rejects before
|
|
584
627
|
* submitting with an `Error` carrying `code === 'RUN_BUSY'` — branch on the
|
package/index.js
CHANGED
|
@@ -16,6 +16,7 @@ function normalizeRunOptions(runOptions) {
|
|
|
16
16
|
generationParams: undefined,
|
|
17
17
|
cacheKey: undefined,
|
|
18
18
|
ephemeral: false,
|
|
19
|
+
baseCacheKey: undefined,
|
|
19
20
|
rejectWhenBusy: undefined,
|
|
20
21
|
};
|
|
21
22
|
}
|
|
@@ -43,6 +44,9 @@ function normalizeRunOptions(runOptions) {
|
|
|
43
44
|
if (options.ephemeral !== undefined && typeof options.ephemeral !== "boolean") {
|
|
44
45
|
throw new TypeError("ephemeral must be a boolean when provided");
|
|
45
46
|
}
|
|
47
|
+
if (options.baseCacheKey !== undefined && typeof options.baseCacheKey !== "string") {
|
|
48
|
+
throw new TypeError("baseCacheKey must be a string when provided");
|
|
49
|
+
}
|
|
46
50
|
if (options.rejectWhenBusy !== undefined && typeof options.rejectWhenBusy !== "boolean") {
|
|
47
51
|
throw new TypeError("rejectWhenBusy must be a boolean when provided");
|
|
48
52
|
}
|
|
@@ -51,15 +55,28 @@ function normalizeRunOptions(runOptions) {
|
|
|
51
55
|
generationParams: normalizeGenerationParams(options.generationParams),
|
|
52
56
|
cacheKey: options.cacheKey,
|
|
53
57
|
ephemeral: options.ephemeral === true,
|
|
58
|
+
baseCacheKey: options.baseCacheKey,
|
|
54
59
|
// Left undefined when unset so admission falls back to the instance default.
|
|
55
60
|
rejectWhenBusy: options.rejectWhenBusy,
|
|
56
61
|
};
|
|
57
62
|
}
|
|
63
|
+
// Malformed parts (null, non-objects) are not byte media: they pass through
|
|
64
|
+
// untouched so the native validator rejects them with `InvalidInputFormat`.
|
|
65
|
+
function isByteMediaPart(part) {
|
|
66
|
+
const candidate = part;
|
|
67
|
+
return (candidate !== null &&
|
|
68
|
+
typeof candidate === "object" &&
|
|
69
|
+
candidate.type === "media" &&
|
|
70
|
+
candidate.content instanceof Uint8Array);
|
|
71
|
+
}
|
|
58
72
|
function promptToAddonMessages(prompt, runOptions) {
|
|
59
73
|
if (!Array.isArray(prompt)) {
|
|
60
74
|
throw new TypeError("Prompt input must be Message[]");
|
|
61
75
|
}
|
|
62
|
-
const { prefill, generationParams, cacheKey, ephemeral } = normalizeRunOptions(runOptions);
|
|
76
|
+
const { prefill, generationParams, cacheKey, ephemeral, baseCacheKey } = normalizeRunOptions(runOptions);
|
|
77
|
+
// Only ever JSON-serialized into the addon's text message: a byte-buffer
|
|
78
|
+
// media part reaches the native side path-less, bound to the hoisted buffer
|
|
79
|
+
// by position, so the wire shape is wider than the public `Message` type.
|
|
63
80
|
const textMessages = [];
|
|
64
81
|
const mediaItems = [];
|
|
65
82
|
for (const message of prompt) {
|
|
@@ -67,10 +84,26 @@ function promptToAddonMessages(prompt, runOptions) {
|
|
|
67
84
|
if (media.role === "user" && media.type === "media" && media.content instanceof Uint8Array) {
|
|
68
85
|
mediaItems.push(media.content);
|
|
69
86
|
textMessages.push({ ...media, content: "" });
|
|
87
|
+
continue;
|
|
70
88
|
}
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
// Ordered parts: hoist each media part's bytes next to the other media
|
|
90
|
+
// messages and leave a path-less part behind. The native side records the
|
|
91
|
+
// plan in marker order, so the bytes still bind to the right markers.
|
|
92
|
+
const text = message;
|
|
93
|
+
if (Array.isArray(text.content)) {
|
|
94
|
+
textMessages.push({
|
|
95
|
+
...text,
|
|
96
|
+
content: text.content.map((part) => {
|
|
97
|
+
if (isByteMediaPart(part)) {
|
|
98
|
+
mediaItems.push(part.content);
|
|
99
|
+
return { type: "media" };
|
|
100
|
+
}
|
|
101
|
+
return part;
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
73
105
|
}
|
|
106
|
+
textMessages.push(message);
|
|
74
107
|
}
|
|
75
108
|
const promptMessages = [];
|
|
76
109
|
for (const mediaData of mediaItems) {
|
|
@@ -83,6 +116,7 @@ function promptToAddonMessages(prompt, runOptions) {
|
|
|
83
116
|
generationParams,
|
|
84
117
|
cacheKey,
|
|
85
118
|
ephemeral,
|
|
119
|
+
baseCacheKey,
|
|
86
120
|
});
|
|
87
121
|
return promptMessages;
|
|
88
122
|
}
|
|
@@ -239,13 +273,24 @@ function normalizeFinetuneParams(opts) {
|
|
|
239
273
|
return out;
|
|
240
274
|
}
|
|
241
275
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* in `GGUFShards::expandGGUFIntoShards`.
|
|
276
|
+
* Validates the single disk path. For a sharded GGUF, pass only the first
|
|
277
|
+
* shard; Fabric discovers the remaining shards. Weight streaming is unsupported.
|
|
245
278
|
*/
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
|
|
279
|
+
function validateModelPath(files) {
|
|
280
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
281
|
+
throw new TypeError("files.model must contain one absolute disk path");
|
|
282
|
+
}
|
|
283
|
+
if (files.length !== 1) {
|
|
284
|
+
throw new TypeError("Weight streaming is no longer supported. Pass only one disk path in files.model; " +
|
|
285
|
+
"for a sharded model, pass only the first shard path. Do not pass a tensor-name file.");
|
|
286
|
+
}
|
|
287
|
+
if (typeof files[0] !== "string" || files[0].length === 0 || !path.isAbsolute(files[0])) {
|
|
288
|
+
throw new TypeError("files.model[0] must be an absolute disk path string; weight buffers are not supported");
|
|
289
|
+
}
|
|
290
|
+
if (files[0].endsWith(".tensors.txt")) {
|
|
291
|
+
throw new TypeError("Weight streaming is no longer supported. Pass the first GGUF shard path, not a tensor-name file.");
|
|
292
|
+
}
|
|
293
|
+
return files[0];
|
|
249
294
|
}
|
|
250
295
|
// Replace media Uint8Array contents with a small summary so the logger never
|
|
251
296
|
// asks V8 to JSON.stringify multi-MB binary blobs.
|
|
@@ -257,12 +302,21 @@ function sanitizePromptForLog(prompt) {
|
|
|
257
302
|
if (media && media.type === "media" && media.content instanceof Uint8Array) {
|
|
258
303
|
return { ...media, content: `[Uint8Array byteLength=${media.content.byteLength}]` };
|
|
259
304
|
}
|
|
305
|
+
const text = msg;
|
|
306
|
+
if (Array.isArray(text?.content)) {
|
|
307
|
+
return {
|
|
308
|
+
...text,
|
|
309
|
+
content: text.content.map((part) => isByteMediaPart(part)
|
|
310
|
+
? { ...part, content: `[Uint8Array byteLength=${part.content.byteLength}]` }
|
|
311
|
+
: part),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
260
314
|
return msg;
|
|
261
315
|
});
|
|
262
316
|
}
|
|
263
317
|
/** LLM client wrapping the native LlamaInterface for inference, finetuning, and pause/resume. */
|
|
264
318
|
const LlmLlamacpp = class LlmLlamacpp {
|
|
265
|
-
static
|
|
319
|
+
static validateModelPath = validateModelPath;
|
|
266
320
|
static assessFit = fit_1.assessFit;
|
|
267
321
|
// Attached for tests; untyped because `typeof QvacResponse` is not nameable by consumers.
|
|
268
322
|
static QvacResponse = infer_base_1.QvacResponse;
|
|
@@ -270,7 +324,7 @@ const LlmLlamacpp = class LlmLlamacpp {
|
|
|
270
324
|
opts;
|
|
271
325
|
logger;
|
|
272
326
|
state;
|
|
273
|
-
|
|
327
|
+
_modelPath;
|
|
274
328
|
_projectionModelPath;
|
|
275
329
|
_config;
|
|
276
330
|
_finetuneJob;
|
|
@@ -281,17 +335,7 @@ const LlmLlamacpp = class LlmLlamacpp {
|
|
|
281
335
|
_batchHandler;
|
|
282
336
|
_checkpointSaveDir;
|
|
283
337
|
constructor({ files, config, logger = null, opts = {} }) {
|
|
284
|
-
|
|
285
|
-
throw new TypeError("files.model must be a non-empty array of absolute paths");
|
|
286
|
-
}
|
|
287
|
-
for (const [i, entry] of files.model.entries()) {
|
|
288
|
-
if (typeof entry !== "string" || entry.length === 0) {
|
|
289
|
-
throw new TypeError(`files.model[${i}] must be an absolute path string`);
|
|
290
|
-
}
|
|
291
|
-
if (!path.isAbsolute(entry)) {
|
|
292
|
-
throw new TypeError(`files.model[${i}] must be an absolute path (got: ${entry})`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
338
|
+
this._modelPath = validateModelPath(files?.model);
|
|
295
339
|
if (files.projectionModel !== undefined) {
|
|
296
340
|
if (typeof files.projectionModel !== "string" || files.projectionModel.length === 0) {
|
|
297
341
|
throw new TypeError("files.projectionModel must be an absolute path string");
|
|
@@ -300,7 +344,6 @@ const LlmLlamacpp = class LlmLlamacpp {
|
|
|
300
344
|
throw new TypeError(`files.projectionModel must be an absolute path (got: ${files.projectionModel})`);
|
|
301
345
|
}
|
|
302
346
|
}
|
|
303
|
-
this._files = files.model;
|
|
304
347
|
this._projectionModelPath = files.projectionModel || "";
|
|
305
348
|
this._config = config;
|
|
306
349
|
this.logger = new QvacLogger(logger);
|
|
@@ -361,18 +404,14 @@ const LlmLlamacpp = class LlmLlamacpp {
|
|
|
361
404
|
}
|
|
362
405
|
async _load() {
|
|
363
406
|
this.logger.info("Starting model load");
|
|
364
|
-
const primaryGgufPath = pickPrimaryGgufPath(this._files);
|
|
365
407
|
const configurationParams = {
|
|
366
|
-
path:
|
|
408
|
+
path: this._modelPath,
|
|
367
409
|
projectionPath: this._projectionModelPath,
|
|
368
410
|
config: { ...this._config },
|
|
369
411
|
};
|
|
370
412
|
this.logger.info("Creating addon with configuration:", configurationParams);
|
|
371
413
|
try {
|
|
372
414
|
this.addon = this._createAddon(configurationParams);
|
|
373
|
-
if (this._files.length > 1) {
|
|
374
|
-
await this._streamShards();
|
|
375
|
-
}
|
|
376
415
|
this.logger.info("Activating addon");
|
|
377
416
|
await this.addon.activate();
|
|
378
417
|
}
|
|
@@ -389,17 +428,6 @@ const LlmLlamacpp = class LlmLlamacpp {
|
|
|
389
428
|
}
|
|
390
429
|
this.logger.info("Model load completed successfully");
|
|
391
430
|
}
|
|
392
|
-
async _streamShards() {
|
|
393
|
-
for (const filePath of this._files) {
|
|
394
|
-
const filename = path.basename(filePath);
|
|
395
|
-
const stream = fs.createReadStream(filePath);
|
|
396
|
-
for await (const chunk of stream) {
|
|
397
|
-
await this.addon.loadWeights({ filename, chunk, completed: false });
|
|
398
|
-
}
|
|
399
|
-
await this.addon.loadWeights({ filename, chunk: null, completed: true });
|
|
400
|
-
this.logger.info(`Streamed weights for ${filename}`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
431
|
run(prompt, runOptions) {
|
|
404
432
|
if (BatchHandler.isBatchInput(prompt)) {
|
|
405
433
|
if (runOptions !== undefined) {
|
|
@@ -863,9 +891,20 @@ const LlmLlamacpp = class LlmLlamacpp {
|
|
|
863
891
|
if (text !== undefined)
|
|
864
892
|
params.text = text;
|
|
865
893
|
// Media bytes are dropped rather than serialized: the native side rejects
|
|
866
|
-
// any media message by its type.
|
|
894
|
+
// any media message or ordered media part by its type.
|
|
867
895
|
if (history !== undefined) {
|
|
868
|
-
params.history = JSON.stringify(history.map((message) =>
|
|
896
|
+
params.history = JSON.stringify(history.map((message) => {
|
|
897
|
+
if (message?.type === "media")
|
|
898
|
+
return { ...message, content: "" };
|
|
899
|
+
const textMessage = message;
|
|
900
|
+
if (Array.isArray(textMessage?.content)) {
|
|
901
|
+
return {
|
|
902
|
+
...message,
|
|
903
|
+
content: textMessage.content.map((part) => isByteMediaPart(part) ? { type: "media" } : part),
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
return message;
|
|
907
|
+
}));
|
|
869
908
|
}
|
|
870
909
|
if (tools !== undefined)
|
|
871
910
|
params.tools = JSON.stringify(tools);
|
|
@@ -877,7 +916,7 @@ const LlmLlamacpp = class LlmLlamacpp {
|
|
|
877
916
|
};
|
|
878
917
|
// Runtime-redundant: ESM named imports need the top-level `module.exports.X =` form.
|
|
879
918
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access -- `module.exports` is untyped CommonJS surface. */
|
|
880
|
-
module.exports.
|
|
919
|
+
module.exports.validateModelPath = validateModelPath;
|
|
881
920
|
module.exports.assessFit = fit_1.assessFit;
|
|
882
921
|
module.exports.QvacResponse = infer_base_1.QvacResponse;
|
|
883
922
|
module.exports = LlmLlamacpp;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qvac/llm-llamacpp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.68.0",
|
|
4
4
|
"description": "llama addon for qvac",
|
|
5
5
|
"addon": true,
|
|
6
6
|
"scripts": {
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
"bugs": "https://github.com/tetherto/qvac/issues",
|
|
80
80
|
"homepage": "https://qvac.tether.io",
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@qvac/fabric-android-arm64": "^0.
|
|
83
|
-
"@qvac/fabric-ios": "^0.
|
|
82
|
+
"@qvac/fabric-android-arm64": "^0.26.0",
|
|
83
|
+
"@qvac/fabric-ios": "^0.26.0",
|
|
84
84
|
"@types/node": "^24.2.1",
|
|
85
85
|
"bare-crypto": "^1.15.3",
|
|
86
86
|
"bare-process": "^4.2.2",
|
|
@@ -102,22 +102,22 @@
|
|
|
102
102
|
"typescript-eslint": "^8.63.0"
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@qvac/fabric": "^0.
|
|
105
|
+
"@qvac/fabric": "^0.26.0",
|
|
106
106
|
"@qvac/infer-base": "^0.8.0",
|
|
107
107
|
"@qvac/logging": "^0.1.0",
|
|
108
108
|
"bare-fs": "^4.5.1",
|
|
109
109
|
"bare-path": "^3.0.0"
|
|
110
110
|
},
|
|
111
111
|
"optionalDependencies": {
|
|
112
|
-
"@qvac/fabric-linux-x64": "^0.
|
|
113
|
-
"@qvac/fabric-linux-arm64": "^0.
|
|
114
|
-
"@qvac/fabric-darwin-arm64": "^0.
|
|
115
|
-
"@qvac/fabric-darwin-x64": "^0.
|
|
116
|
-
"@qvac/fabric-win32-x64": "^0.
|
|
112
|
+
"@qvac/fabric-linux-x64": "^0.26.0",
|
|
113
|
+
"@qvac/fabric-linux-arm64": "^0.26.0",
|
|
114
|
+
"@qvac/fabric-darwin-arm64": "^0.26.0",
|
|
115
|
+
"@qvac/fabric-darwin-x64": "^0.26.0",
|
|
116
|
+
"@qvac/fabric-win32-x64": "^0.26.0"
|
|
117
117
|
},
|
|
118
118
|
"peerDependencies": {
|
|
119
|
-
"@qvac/fabric-android-arm64": "^0.
|
|
120
|
-
"@qvac/fabric-ios": "^0.
|
|
119
|
+
"@qvac/fabric-android-arm64": "^0.26.0",
|
|
120
|
+
"@qvac/fabric-ios": "^0.26.0"
|
|
121
121
|
},
|
|
122
122
|
"peerDependenciesMeta": {
|
|
123
123
|
"@qvac/fabric-android-arm64": {
|
|
Binary file
|
|
Binary file
|