@qvac/llm-llamacpp 0.65.1 → 0.67.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 +73 -17
- package/addon.d.ts +12 -0
- package/addon.js +8 -0
- package/fit.d.ts +21 -20
- package/fit.js +1 -2
- package/index.d.ts +82 -8
- package/index.js +102 -3
- package/package.json +1 -1
- 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 +67 -33
- package/prebuilds/darwin-x64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/darwin-x64/qvac__llm-llamacpp.bare.exports +107 -35
- package/prebuilds/ios-arm64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/ios-arm64/qvac__llm-llamacpp.bare.exports +67 -33
- package/prebuilds/ios-arm64-simulator/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/ios-arm64-simulator/qvac__llm-llamacpp.bare.exports +67 -33
- package/prebuilds/ios-x64-simulator/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/ios-x64-simulator/qvac__llm-llamacpp.bare.exports +110 -38
- 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)
|
|
@@ -206,7 +207,8 @@ const config = {
|
|
|
206
207
|
| 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
208
|
| 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
209
|
| 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)) |
|
|
210
|
+
| 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)) |
|
|
211
|
+
| 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
212
|
| 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
213
|
| 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
214
|
| cache-type-v | `f16`, `f32`, `bf16`, `q8_0`, `q4_0`, … | auto (see below) | KV-cache **value** quantization type. Quantizing V requires `flash-attn` on |
|
|
@@ -265,7 +267,7 @@ projector runs on CPU. The resolved choice is logged at verbosity ≥ 2 as
|
|
|
265
267
|
|
|
266
268
|
`spec-type: 'draft-mtp'` turns on llama-server's multi-token-prediction speculative decoding for models that ship their MTP layers (`blk.N.nextn.*`, e.g. Qwen3.5 / Qwen3.6 GGUFs that keep them; quantizations that strip them fail to load with this option unless an external head is given, see below). Every step the MTP head drafts up to `spec-draft-n-max` tokens from the model's own hidden states, the model verifies the sampled token and the draft in one decode, and the longest matching prefix plus one new token is kept. The sampler decides every kept token, so sampling settings, grammars and stop conditions behave as without speculation. The implementation follows llama-server step by step: the same draft limits, rollback by trimming or by a checkpoint for memory that cannot trim, and the same replay.
|
|
267
269
|
|
|
268
|
-
For a model without MTP layers, give an external MTP head as `spec-draft-model` (llama-server's `--spec-type draft-mtp -md <head>`): a GGUF with MTP layers for the same model, such as the MTP quantization of it, or a Gemma 4 assistant for its Gemma 4 model. The head is loaded next to the model and placed like it (`device`, `gpu-layers`), and the load's fit and `assessFit` count its weights as well as its context. A head without MTP layers fails the load, and `assessFit`
|
|
270
|
+
For a model without MTP layers, give an external MTP head as `spec-draft-model` (llama-server's `--spec-type draft-mtp -md <head>`): a GGUF with MTP layers for the same model, such as the MTP quantization of it, or a Gemma 4 assistant for its Gemma 4 model. The head is loaded next to the model and placed like it (`device`, `gpu-layers`), and the load's fit and `assessFit` count its weights as well as its context. A head without MTP layers fails the load, and `assessFit` rejects it. A head for another model, with a different hidden width or vocabulary, also fails the load, and `assessFit` rejects it the same way. A Gemma 4 assistant reads the model's own tensors, so fabric measures it next to the model, as the load creates it.
|
|
269
271
|
|
|
270
272
|
`spec-type: 'draft-dflash'` drafts with a separate DFlash model instead, given as `spec-draft-model` (an absolute path; llama-server's `-md`). The DFlash model reads the hidden states of the target layers it was trained on and denoises a whole block of draft tokens in one pass, so a step drafts up to its block size minus one tokens (`spec-draft-n-max` caps it further). It is loaded next to the model and placed like it (`device`, `gpu-layers`). The load's fit and `assessFit` count its weights and context, measured next to the model when the drafter borrows the model's embeddings and output head. A draft model fabric still cannot measure is left out of both, and fabric logs `failed to measure the memory of the extra model, fitting without it`. Everything else (verification, rollback, batching, caches, stats) is the same as for MTP.
|
|
271
273
|
|
|
@@ -345,6 +347,28 @@ const response = await model.run([
|
|
|
345
347
|
|
|
346
348
|
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
349
|
|
|
350
|
+
#### Ordered text and media
|
|
351
|
+
|
|
352
|
+
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:
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
const response = await model.run([{
|
|
356
|
+
role: 'user',
|
|
357
|
+
content: [
|
|
358
|
+
{ type: 'text', text: 'Which image shows the same place as ' },
|
|
359
|
+
{ type: 'media', content: '/images/place.jpg' },
|
|
360
|
+
{ type: 'text', text: '?\nA. ' },
|
|
361
|
+
{ type: 'media', content: imageABytes }, // raw bytes work too
|
|
362
|
+
{ type: 'text', text: '\nB. ' },
|
|
363
|
+
{ type: 'media', content: '/images/option-b.jpg' }
|
|
364
|
+
]
|
|
365
|
+
}])
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
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`.
|
|
369
|
+
|
|
370
|
+
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.
|
|
371
|
+
|
|
348
372
|
#### Batch inference
|
|
349
373
|
|
|
350
374
|
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.
|
|
@@ -384,6 +408,30 @@ const response = await model.run([
|
|
|
384
408
|
])
|
|
385
409
|
```
|
|
386
410
|
|
|
411
|
+
#### Counting tokens
|
|
412
|
+
|
|
413
|
+
`calcTokens` returns how many tokens an input takes on the loaded model, without running it:
|
|
414
|
+
|
|
415
|
+
```javascript
|
|
416
|
+
const raw = await model.calcTokens({ text: 'Hello, world!' })
|
|
417
|
+
|
|
418
|
+
const prompt = await model.calcTokens({
|
|
419
|
+
history: [{ role: 'system', content: 'You are a helpful assistant.' }],
|
|
420
|
+
tools: [{ type: 'function', name: 'get_weather', parameters: { type: 'object', properties: { city: { type: 'string' } } } }],
|
|
421
|
+
text: 'Weather in Paris?'
|
|
422
|
+
})
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
- `text` alone is counted raw, with no chat template and no BOS.
|
|
426
|
+
- `history` takes the `Message[]` that `run()` takes, and `tools` takes tool definitions. Either one is rendered by the chat template as the opening request of a fresh conversation, with `text` (even empty) appended as the last user turn. The count includes BOS, and the generation prompt when the last turn is a user or tool turn, so it equals the `promptTokens` a fresh `run()` of the same conversation reports. Tools the template drops are not counted. On an encoder-decoder model `run()` encodes the prompt separately, so its `promptTokens` differs from this count.
|
|
427
|
+
- `tools` needs `text` or `history`, as `run()` can't render tools without a message.
|
|
428
|
+
- It never queues behind `run()`, so it answers while requests are generating.
|
|
429
|
+
- Errors:
|
|
430
|
+
- A wrong-typed argument rejects with a `TypeError`.
|
|
431
|
+
- Before `load()` or after `unload()` it rejects with `Model is not loaded`.
|
|
432
|
+
- Other invalid input, media included, rejects with an error whose `code` is `InvalidArgument` and whose message starts with `calcTokens:`.
|
|
433
|
+
- A conversation the chat template cannot render at all fails as it would in `run()`.
|
|
434
|
+
|
|
387
435
|
### 7. Release Resources
|
|
388
436
|
|
|
389
437
|
Unload the model when finished:
|
|
@@ -398,7 +446,7 @@ try {
|
|
|
398
446
|
|
|
399
447
|
### API behavior by state
|
|
400
448
|
|
|
401
|
-
The following table describes the expected behavior of `run` and `
|
|
449
|
+
The following table describes the expected behavior of `run`, `cancel` and `calcTokens` depending on the current state (idle vs jobs running). The two cancel entry points have different scopes:
|
|
402
450
|
|
|
403
451
|
- `response.cancel()` — **targeted**: cancels only the job (or batch group) that `run()` call produced, leaving other concurrent jobs running.
|
|
404
452
|
- `model.cancel()` — **global**: cancels every live job (in-flight and queued) at the moment of the call, plus any finetuning in progress.
|
|
@@ -411,6 +459,7 @@ The following table describes the expected behavior of `run` and `cancel` depend
|
|
|
411
459
|
| busy, `parallel >= 2` (default `rejectWhenBusy: false`) | run | **Allowed** — the job is admitted concurrently (continuous batching) or queued until a slot frees; each call gets its own independent `QvacResponse` |
|
|
412
460
|
| busy | `response.cancel()` | **Allowed** — cancels only that response's job/group; Promise resolves when it has stopped |
|
|
413
461
|
| busy | `model.cancel()` | **Allowed** — cancels all live jobs; Promise resolves when they have stopped |
|
|
462
|
+
| idle or busy | `calcTokens` | **Allowed** — counts without running and never queues behind a job; see [Counting tokens](#counting-tokens) |
|
|
414
463
|
|
|
415
464
|
Admission is controlled by `rejectWhenBusy` (instance-level `opts.rejectWhenBusy`, overridable per call via `runOptions.rejectWhenBusy`). Its default follows `parallel`: `true` for `parallel: 1` (busy runs fail fast, preserving the historical single-job contract) and `false` for `parallel >= 2` (busy runs queue behind the pool and start as slots free). With `parallel >= 2`, separate top-level `run()` calls are batched together into the same decode loop — see [Continuous Batching](./docs/continuous-batching.md).
|
|
416
465
|
|
|
@@ -418,7 +467,7 @@ Admission is controlled by `rejectWhenBusy` (instance-level `opts.rejectWhenBusy
|
|
|
418
467
|
|
|
419
468
|
#### Prefill (cache warming) with `parallel >= 2`
|
|
420
469
|
|
|
421
|
-
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
|
|
470
|
+
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).
|
|
422
471
|
|
|
423
472
|
#### Cancelling a batch
|
|
424
473
|
|
|
@@ -444,26 +493,33 @@ const fit = await LlmLlamacpp.assessFit({
|
|
|
444
493
|
config: { device: 'gpu', 'ctx-size': '4096' }
|
|
445
494
|
})
|
|
446
495
|
|
|
447
|
-
fit.status // 'fits' | 'does-not-fit' | '
|
|
448
|
-
fit.
|
|
449
|
-
fit.
|
|
450
|
-
fit.ctxSize
|
|
451
|
-
fit.devices // one row per device the model was assigned to, then a `host` row
|
|
452
|
-
fit.deviceBytes // model, context and compute summed across the devices, host excluded
|
|
453
|
-
fit.hostBytes // the same, for the trailing host row
|
|
496
|
+
fit.status // 'fits' | 'does-not-fit' | 'unknown'
|
|
497
|
+
fit.devices // one row per device the model is assigned to, then the host CPU
|
|
498
|
+
fit.devices[0] // { id, name, unified, usedBytes, freeBytes, totalBytes }
|
|
499
|
+
fit.stats // { ctxSize, gpuLayers }
|
|
454
500
|
```
|
|
455
501
|
|
|
456
|
-
|
|
502
|
+
Each device row gives the memory the load takes on that device, as `usedBytes`, and the device's free and total memory. `id` is the PCI bus ID when the backend reports one, and the device name otherwise. `unified` is true for memory the host owns: the CPU, an integrated GPU, a GPU that shares the host's memory, Android's OpenCL GPU, and a device that reports no memory of its own, which takes the host's figures. With `load_mode: 'mmap'`, the host row leaves out the mapped weights, which the OS can evict.
|
|
503
|
+
|
|
504
|
+
On an iOS device every unified row shares the app's own memory limit, `os_proc_available_memory()`. Each such row's free memory is capped at that limit, and a load whose unified rows together take more than the limit less the margin does not fit. A limit of 0 means the app has none left.
|
|
505
|
+
|
|
506
|
+
`stats` is the load as the fit resolved it. On `fits`, `ctxSize` and `gpuLayers` are the context and GPU layer count the fitter settled on, which can be smaller than the load asked for. On `does-not-fit` they are the ones the load asked for.
|
|
507
|
+
|
|
508
|
+
`status` is `unknown` for a load the fitter cannot project: one placed on the CPU, one with a LoRA adapter or a multimodal projector, one with `fit` turned off, or one the fitter cannot break down per device, the host CPU included. `devices` is then empty and `stats` is `{}`.
|
|
509
|
+
|
|
510
|
+
The projection reads the model's vocabulary, and llama asserts on a vocabulary it finds inconsistent. An assert aborts the process instead of rejecting, so take the model from a source you trust, or call `assessFit` behind a process boundary.
|
|
511
|
+
|
|
512
|
+
`config` is the load, exactly as `loadModel` takes it. The projection resolves it with the same code that load runs, so every setting reaches the fitter the way it would reach the engine, and a load the engine would refuse rejects here. A setting left out takes llama's default.
|
|
457
513
|
|
|
458
|
-
With `spec-type: 'draft-mtp'` the MTP draft context is fitted next to the model, as the load fits it, and its memory is in the verdict and in the
|
|
514
|
+
With `spec-type: 'draft-mtp'` the MTP draft context is fitted next to the model, as the load fits it, and its memory is in the verdict and in the device rows; on a model or head without MTP layers the load fails, so the projection rejects. A DFlash draft model or a Gemma 4 assistant is fitted the same way, measured next to the model since it reads the model's tensors, and is in the verdict and the rows too (see [Speculative decoding](#speculative-decoding-mtp-dflash)).
|
|
459
515
|
|
|
460
|
-
Pinning `gpu-layers` fixes the placement: the fitter
|
|
516
|
+
Pinning `gpu-layers` fixes the placement: the fitter keeps layers on the devices the load claimed, so the answer is for that exact placement. Leave it unset for a projection that can place the model itself.
|
|
461
517
|
|
|
462
|
-
`minCtxSize` sets a floor the fitter may not reduce the context below, defaulting to llama's own `fit-ctx` of 4096. `marginBytes` is the memory to leave free on every device
|
|
518
|
+
`minCtxSize` sets a floor the fitter may not reduce the context below, defaulting to llama's own `fit-ctx` of 4096. `marginBytes` is the memory to leave free on every device. The load's own `fit-target`, 1 GiB per device by default, applies too, and the larger of the two binds. `backendsDir` is where the dynamically-loaded ggml backends live.
|
|
463
519
|
|
|
464
520
|
`assessFit` returns a Promise. Concurrent calls run one at a time within this addon.
|
|
465
521
|
|
|
466
|
-
A model the fitter cannot read
|
|
522
|
+
A model the fitter cannot read, an invalid request, or a machine with no backend device rejects with an error whose message starts with `assessFit:` and names the cause.
|
|
467
523
|
|
|
468
524
|
## Fine-tuning
|
|
469
525
|
|
|
@@ -703,7 +759,7 @@ npm run test:cpp:models:ci # exactly what .github/workflows/cpp-tests-llm.yml
|
|
|
703
759
|
# downloads; matches what CI exercises
|
|
704
760
|
```
|
|
705
761
|
|
|
706
|
-
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`).
|
|
762
|
+
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`).
|
|
707
763
|
|
|
708
764
|
### Fuzzing
|
|
709
765
|
|
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";
|
|
@@ -65,6 +66,12 @@ export interface AddonConfigurationParams {
|
|
|
65
66
|
config: Record<string, unknown>;
|
|
66
67
|
}
|
|
67
68
|
export type AddonOutputCallback = (addon: unknown, event: unknown, data: unknown, error: unknown, jobId: unknown) => void;
|
|
69
|
+
/** `calcTokens` input as the native binding takes it: `history` and `tools` are JSON array text. */
|
|
70
|
+
export interface AddonCalcTokensParams {
|
|
71
|
+
text?: string;
|
|
72
|
+
history?: string;
|
|
73
|
+
tools?: string;
|
|
74
|
+
}
|
|
68
75
|
export interface LlamaBinding {
|
|
69
76
|
createInstance(owner: LlamaInterface, configurationParams: AddonConfigurationParams, outputCallback: AddonOutputCallback, reserved: null): object;
|
|
70
77
|
loadWeights(handle: unknown, data: LoadWeightsData): Promise<void> | void;
|
|
@@ -76,6 +83,7 @@ export interface LlamaBinding {
|
|
|
76
83
|
finetune?(handle: unknown, params: FinetuneOptions): Promise<number | false> | number | false;
|
|
77
84
|
saveCache(handle: unknown, cacheKey: string): Promise<void>;
|
|
78
85
|
discardCache(handle: unknown, cacheKey: string): Promise<void>;
|
|
86
|
+
calcTokens(handle: unknown, params: AddonCalcTokensParams): Promise<number>;
|
|
79
87
|
runJob(handle: unknown, data: AddonRunJobMessage[]): Promise<AddonRunJobResult>;
|
|
80
88
|
runJob(handle: unknown, data: AddonBatchRunItem[]): Promise<AddonBatchRunResult>;
|
|
81
89
|
destroyInstance(handle: unknown): void;
|
|
@@ -138,6 +146,10 @@ export declare class LlamaInterface {
|
|
|
138
146
|
* (see `LlmLlamacpp.discardCache`).
|
|
139
147
|
*/
|
|
140
148
|
discardCache(cacheKey: string): Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* Tokens `params` takes on the loaded model (see `LlmLlamacpp.calcTokens`).
|
|
151
|
+
*/
|
|
152
|
+
calcTokens(params: AddonCalcTokensParams): Promise<number>;
|
|
141
153
|
/**
|
|
142
154
|
* Run finetuning when native binding provides support.
|
|
143
155
|
*/
|
package/addon.js
CHANGED
|
@@ -154,6 +154,14 @@ class LlamaInterface {
|
|
|
154
154
|
return Promise.reject(new Error("Model is not loaded"));
|
|
155
155
|
return Promise.resolve(this._binding.discardCache(this._handle, cacheKey));
|
|
156
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Tokens `params` takes on the loaded model (see `LlmLlamacpp.calcTokens`).
|
|
159
|
+
*/
|
|
160
|
+
calcTokens(params) {
|
|
161
|
+
if (!this._handle)
|
|
162
|
+
return Promise.reject(new Error("Model is not loaded"));
|
|
163
|
+
return Promise.resolve(this._binding.calcTokens(this._handle, params));
|
|
164
|
+
}
|
|
157
165
|
/**
|
|
158
166
|
* Run finetuning when native binding provides support.
|
|
159
167
|
*/
|
package/fit.d.ts
CHANGED
|
@@ -5,41 +5,42 @@ export interface LlamaFitRequest {
|
|
|
5
5
|
config?: Record<string, string>;
|
|
6
6
|
/** Floor the fitter may not reduce an unpinned context below. */
|
|
7
7
|
minCtxSize?: number;
|
|
8
|
-
/** Memory to leave free on every device
|
|
8
|
+
/** Memory to leave free on every device. The larger of this and the load's own `fit-target` binds. */
|
|
9
9
|
marginBytes?: number;
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
export type
|
|
11
|
+
/** `unknown` is a load the fitter cannot project, such as one placed on the CPU. */
|
|
12
|
+
export type LlamaFitStatus = 'fits' | 'does-not-fit' | 'unknown';
|
|
13
13
|
export interface LlamaFitDevice {
|
|
14
|
+
/** The PCI bus ID when the backend reports one, the device name otherwise. */
|
|
15
|
+
id: string;
|
|
14
16
|
name: string;
|
|
15
|
-
|
|
17
|
+
unified: boolean;
|
|
18
|
+
/** Model, context and compute memory the load takes on this device. */
|
|
19
|
+
usedBytes: number;
|
|
16
20
|
freeBytes: number;
|
|
17
|
-
|
|
18
|
-
contextBytes: number;
|
|
19
|
-
computeBytes: number;
|
|
21
|
+
totalBytes: number;
|
|
20
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The load as the fit resolved it: on `fits` the context and GPU layer count the
|
|
25
|
+
* fitter settled on, on `does-not-fit` the ones the load asked for. Empty when
|
|
26
|
+
* `unknown`.
|
|
27
|
+
*/
|
|
28
|
+
export type LlamaFitStats = {
|
|
29
|
+
ctxSize: number;
|
|
30
|
+
gpuLayers: number;
|
|
31
|
+
} | Record<string, never>;
|
|
21
32
|
export interface LlamaFitResult {
|
|
22
33
|
status: LlamaFitStatus;
|
|
23
|
-
|
|
24
|
-
/** The layer count the placement resolved to. */
|
|
25
|
-
gpuLayers: number;
|
|
26
|
-
ctxSize: number;
|
|
27
|
-
/** One row per device the model was assigned to, then a `host` row. */
|
|
34
|
+
/** One row per device the model is assigned to, then the host CPU. Empty when `unknown`. */
|
|
28
35
|
devices: LlamaFitDevice[];
|
|
29
|
-
|
|
30
|
-
deviceBytes: number;
|
|
31
|
-
/** The same, for the trailing host row. */
|
|
32
|
-
hostBytes: number;
|
|
33
|
-
trainCtxSize: number;
|
|
34
|
-
expertCount: number;
|
|
36
|
+
stats: LlamaFitStats;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* Projects one model against the memory free right now, reading GGUF metadata
|
|
38
40
|
* and never weight data. The registry's weightless copy of a model answers the
|
|
39
41
|
* same as the model itself, so this can run before anything is downloaded.
|
|
40
42
|
*
|
|
41
|
-
* A model the fitter cannot read
|
|
42
|
-
* request rejects.
|
|
43
|
+
* A model the fitter cannot read, or an invalid request, rejects.
|
|
43
44
|
*
|
|
44
45
|
* Builds the model twice without allocating weights. Concurrent calls run one
|
|
45
46
|
* at a time.
|
package/fit.js
CHANGED
|
@@ -6,8 +6,7 @@ exports.assessFit = assessFit;
|
|
|
6
6
|
* and never weight data. The registry's weightless copy of a model answers the
|
|
7
7
|
* same as the model itself, so this can run before anything is downloaded.
|
|
8
8
|
*
|
|
9
|
-
* A model the fitter cannot read
|
|
10
|
-
* request rejects.
|
|
9
|
+
* A model the fitter cannot read, or an invalid request, rejects.
|
|
11
10
|
*
|
|
12
11
|
* Builds the model twice without allocating weights. Concurrent calls run one
|
|
13
12
|
* at a time.
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import QvacLogger = require("@qvac/logging");
|
|
2
2
|
import { QvacResponse } from "@qvac/infer-base";
|
|
3
|
-
import { assessFit as assessFitImpl, type LlamaFitDevice, type LlamaFitRequest, type LlamaFitResult, type LlamaFitStatus } from "./fit";
|
|
3
|
+
import { assessFit as assessFitImpl, type LlamaFitDevice, type LlamaFitRequest, type LlamaFitResult, type LlamaFitStats, type LlamaFitStatus } from "./fit";
|
|
4
4
|
import type * as AddonModule from "./addon";
|
|
5
5
|
type BareEventMap = Record<string | symbol, unknown[]>;
|
|
6
6
|
/** Aliases: inside the namespace, `QvacResponse` resolves to its own member. */
|
|
@@ -9,6 +9,7 @@ type InferQvacResponseOf<Output> = QvacResponse<Output>;
|
|
|
9
9
|
/** Aliases so the class expression's body can name the namespace types below. */
|
|
10
10
|
type BatchPrompt = LlmLlamacpp.BatchPrompt;
|
|
11
11
|
type BatchResponse = LlmLlamacpp.BatchResponse;
|
|
12
|
+
type CalcTokensInput = LlmLlamacpp.CalcTokensInput;
|
|
12
13
|
type FinetuneHandle = LlmLlamacpp.FinetuneHandle;
|
|
13
14
|
type FinetuneOptions = LlmLlamacpp.FinetuneOptions;
|
|
14
15
|
type LlmLlamacppArgs = LlmLlamacpp.LlmLlamacppArgs;
|
|
@@ -70,6 +71,21 @@ interface LlmLlamacpp {
|
|
|
70
71
|
* remove the conversation from disk.
|
|
71
72
|
*/
|
|
72
73
|
discardCache(cacheKey: string): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Tokens `input` takes on the loaded model, without running it. `text`
|
|
76
|
+
* alone is counted raw, with no chat template and no BOS. With `history` or
|
|
77
|
+
* `tools` the conversation, `text` (even empty) appended as the last user
|
|
78
|
+
* turn, is rendered by the chat template as the opening request of a fresh
|
|
79
|
+
* conversation, with BOS and the generation prompt, as `run()` renders it.
|
|
80
|
+
* Tools the template leaves out are not counted. `tools` alone has no
|
|
81
|
+
* message to render and rejects, as `run()` does. Never queues behind
|
|
82
|
+
* `run()`: it reads only load-time state. A wrong-typed argument rejects
|
|
83
|
+
* with `TypeError`, and before `load()` it rejects with `Model is not
|
|
84
|
+
* loaded`. Other invalid input, a media message included, rejects with code
|
|
85
|
+
* `InvalidArgument`; a conversation the chat template cannot render fails
|
|
86
|
+
* as it would in `run()`.
|
|
87
|
+
*/
|
|
88
|
+
calcTokens(input: CalcTokensInput): Promise<number>;
|
|
73
89
|
getState(): {
|
|
74
90
|
configLoaded: boolean;
|
|
75
91
|
};
|
|
@@ -364,6 +380,19 @@ declare namespace LlmLlamacpp {
|
|
|
364
380
|
* maximum is 1048576 (1 TiB). Also accepted as `cache-ram-mib`.
|
|
365
381
|
*/
|
|
366
382
|
cache_ram_mib?: NumericLike;
|
|
383
|
+
/**
|
|
384
|
+
* Whether a request on a decoder-only model may start from state another
|
|
385
|
+
* conversation left: the memory a request without a `cacheKey` keeps,
|
|
386
|
+
* another key's state in a sequence or the RAM tier, or a `cacheKey` file
|
|
387
|
+
* the model has used. `'true'` (default) shares it, as llama-server's
|
|
388
|
+
* prompt cache does. `'false'` keeps every request to its own key's state,
|
|
389
|
+
* for a model serving callers who must not learn through `cacheTokens` or
|
|
390
|
+
* timing whether another conversation shares their prompt: a request
|
|
391
|
+
* without a key starts cold and is reset afterwards, a new key starts cold,
|
|
392
|
+
* and `baseCacheKey` has no effect. Also accepted as
|
|
393
|
+
* `cache-share-across-keys`.
|
|
394
|
+
*/
|
|
395
|
+
cache_share_across_keys?: "true" | "false" | "on" | "off" | "1" | "0";
|
|
367
396
|
/**
|
|
368
397
|
* Speculative decoding, as llama-server's `--spec-type`. `'draft-mtp'`
|
|
369
398
|
* drafts with the model's own multi-token-prediction (MTP / NextN) head,
|
|
@@ -421,9 +450,30 @@ declare namespace LlmLlamacpp {
|
|
|
421
450
|
rejectWhenBusy?: boolean;
|
|
422
451
|
};
|
|
423
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* One ordered piece of a message's `content`. Media parts keep their place in
|
|
455
|
+
* the turn: a single user message can interleave text and media, which a
|
|
456
|
+
* string `content` plus separate `type: "media"` messages cannot express
|
|
457
|
+
* without splitting the turn.
|
|
458
|
+
*/
|
|
459
|
+
type UserContentPart = {
|
|
460
|
+
type: "text";
|
|
461
|
+
text: string;
|
|
462
|
+
} | {
|
|
463
|
+
type: "media";
|
|
464
|
+
/**
|
|
465
|
+
* Either an absolute path to the media file (`string`) or its raw bytes
|
|
466
|
+
* (`Uint8Array`), the same two forms a `type: "media"` message accepts.
|
|
467
|
+
*/
|
|
468
|
+
content: string | Uint8Array;
|
|
469
|
+
};
|
|
424
470
|
interface UserTextMessage {
|
|
425
471
|
role: "system" | "assistant" | "user" | "tool" | "session" | string;
|
|
426
|
-
|
|
472
|
+
/**
|
|
473
|
+
* Plain text, or ordered text/media parts rendered as one turn. Media parts
|
|
474
|
+
* are only valid on user messages.
|
|
475
|
+
*/
|
|
476
|
+
content: string | UserContentPart[];
|
|
427
477
|
/**
|
|
428
478
|
* An assistant turn's reasoning, separate from its answer in `content`, as
|
|
429
479
|
* in OpenAI-compatible APIs. Optional: an answer sent back with its
|
|
@@ -467,6 +517,15 @@ declare namespace LlmLlamacpp {
|
|
|
467
517
|
parameters?: Record<string, any>;
|
|
468
518
|
}
|
|
469
519
|
type Message = UserTextMessage | UserMediaMessage | ChatFunctionDefinition;
|
|
520
|
+
/** What `calcTokens` counts. At least one field is required; `tools` needs `text` or `history`. */
|
|
521
|
+
interface CalcTokensInput {
|
|
522
|
+
/** Counted raw on its own; with `history` or `tools`, the last user turn. */
|
|
523
|
+
text?: string;
|
|
524
|
+
/** A conversation in `run()`'s `Message[]` shape, tool definitions allowed. */
|
|
525
|
+
history?: Message[];
|
|
526
|
+
/** Tool definitions, as `run()` takes them. A name also in `history` rejects. */
|
|
527
|
+
tools?: ChatFunctionDefinition[];
|
|
528
|
+
}
|
|
470
529
|
interface GenerationParams {
|
|
471
530
|
temp?: number;
|
|
472
531
|
top_p?: number;
|
|
@@ -529,10 +588,9 @@ declare namespace LlmLlamacpp {
|
|
|
529
588
|
* Run prefill only (cache warming): the prompt is evaluated but no tokens
|
|
530
589
|
* are generated. On a model loaded with `parallel >= 2` a prefill needs a
|
|
531
590
|
* `cacheKey` (the warmed conversation stays in its slot for the next
|
|
532
|
-
* request on that key) — a keyless prefill warms
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
* batch item.
|
|
591
|
+
* request on that key) — a keyless prefill warms state no later request
|
|
592
|
+
* can count on and is rejected with `InvalidArgument`; run keyless
|
|
593
|
+
* prefills on a `parallel: 1` model. The same rule applies per batch item.
|
|
536
594
|
*/
|
|
537
595
|
prefill?: boolean;
|
|
538
596
|
generationParams?: GenerationParams;
|
|
@@ -549,11 +607,26 @@ declare namespace LlmLlamacpp {
|
|
|
549
607
|
* otherwise be lost with turns the file lacks: when it is set aside for
|
|
550
608
|
* another key, evicted from its batch slot or from the RAM tier, or the
|
|
551
609
|
* model is reloaded or unloaded. With `ephemeral: true` each of those
|
|
552
|
-
* drops it instead, so its next request
|
|
553
|
-
*
|
|
610
|
+
* drops it instead, so its next request finds no state of its own (it
|
|
611
|
+
* still reuses any prefix it shares with what the model holds, except on
|
|
612
|
+
* encoder-decoder models). It still loads an existing file, and an
|
|
613
|
+
* explicit `saveCache(cacheKey)` still writes it.
|
|
554
614
|
* The flag belongs to the conversation's latest request.
|
|
555
615
|
*/
|
|
556
616
|
ephemeral?: boolean;
|
|
617
|
+
/**
|
|
618
|
+
* A `cacheKey` file to start from when nothing in memory serves more of
|
|
619
|
+
* the prompt, such as a system prompt and tools primed with `prefill`.
|
|
620
|
+
* It is read, never written: the request's own state goes to its
|
|
621
|
+
* `cacheKey`. A file that is missing or shares no prefix with the prompt
|
|
622
|
+
* is skipped. Once named, the file stays a source for later requests on
|
|
623
|
+
* this model, as any `cacheKey` file it used does, until files named
|
|
624
|
+
* or picked as a prompt's closest match more recently push it out (the
|
|
625
|
+
* model keeps the 256 most recent, as far as 64 MiB of memory holds
|
|
626
|
+
* their prompt records). No effect with `cache_share_across_keys:
|
|
627
|
+
* 'false'`.
|
|
628
|
+
*/
|
|
629
|
+
baseCacheKey?: string;
|
|
557
630
|
/**
|
|
558
631
|
* Admission policy when the worker pool is full. `true` rejects before
|
|
559
632
|
* submitting with an `Error` carrying `code === 'RUN_BUSY'` — branch on the
|
|
@@ -763,5 +836,6 @@ declare namespace LlmLlamacpp {
|
|
|
763
836
|
type FitResult = LlamaFitResult;
|
|
764
837
|
type FitStatus = LlamaFitStatus;
|
|
765
838
|
type FitDevice = LlamaFitDevice;
|
|
839
|
+
type FitStats = LlamaFitStats;
|
|
766
840
|
}
|
|
767
841
|
export = LlmLlamacpp;
|
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
|
}
|
|
@@ -257,6 +291,15 @@ function sanitizePromptForLog(prompt) {
|
|
|
257
291
|
if (media && media.type === "media" && media.content instanceof Uint8Array) {
|
|
258
292
|
return { ...media, content: `[Uint8Array byteLength=${media.content.byteLength}]` };
|
|
259
293
|
}
|
|
294
|
+
const text = msg;
|
|
295
|
+
if (Array.isArray(text?.content)) {
|
|
296
|
+
return {
|
|
297
|
+
...text,
|
|
298
|
+
content: text.content.map((part) => isByteMediaPart(part)
|
|
299
|
+
? { ...part, content: `[Uint8Array byteLength=${part.content.byteLength}]` }
|
|
300
|
+
: part),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
260
303
|
return msg;
|
|
261
304
|
});
|
|
262
305
|
}
|
|
@@ -826,6 +869,62 @@ const LlmLlamacpp = class LlmLlamacpp {
|
|
|
826
869
|
}
|
|
827
870
|
await this.addon.discardCache(cacheKey);
|
|
828
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* Tokens `input` takes on the loaded model, without running it. `text`
|
|
874
|
+
* alone is counted raw, with no chat template and no BOS. With `history` or
|
|
875
|
+
* `tools` the conversation, `text` (even empty) appended as the last user
|
|
876
|
+
* turn, is rendered by the chat template as the opening request of a fresh
|
|
877
|
+
* conversation, with BOS and the generation prompt, as `run()` renders it.
|
|
878
|
+
* Tools the template leaves out are not counted. `tools` alone has no
|
|
879
|
+
* message to render and rejects, as `run()` does. Never queues behind
|
|
880
|
+
* `run()`: it reads only load-time state. A wrong-typed argument rejects
|
|
881
|
+
* with `TypeError`, and before `load()` it rejects with `Model is not
|
|
882
|
+
* loaded`. Other invalid input, a media message included, rejects with code
|
|
883
|
+
* `InvalidArgument`; a conversation the chat template cannot render fails
|
|
884
|
+
* as it would in `run()`.
|
|
885
|
+
*/
|
|
886
|
+
async calcTokens(input) {
|
|
887
|
+
if (input === null || typeof input !== "object") {
|
|
888
|
+
throw new TypeError("calcTokens(input) requires an object");
|
|
889
|
+
}
|
|
890
|
+
const text = input.text ?? undefined;
|
|
891
|
+
const history = input.history ?? undefined;
|
|
892
|
+
const tools = input.tools ?? undefined;
|
|
893
|
+
if (text !== undefined && typeof text !== "string") {
|
|
894
|
+
throw new TypeError("calcTokens: text must be a string");
|
|
895
|
+
}
|
|
896
|
+
if (history !== undefined && !Array.isArray(history)) {
|
|
897
|
+
throw new TypeError("calcTokens: history must be a Message[]");
|
|
898
|
+
}
|
|
899
|
+
if (tools !== undefined && !Array.isArray(tools)) {
|
|
900
|
+
throw new TypeError("calcTokens: tools must be an array of function definitions");
|
|
901
|
+
}
|
|
902
|
+
if (!this.addon) {
|
|
903
|
+
throw new Error("Model is not loaded");
|
|
904
|
+
}
|
|
905
|
+
const params = {};
|
|
906
|
+
if (text !== undefined)
|
|
907
|
+
params.text = text;
|
|
908
|
+
// Media bytes are dropped rather than serialized: the native side rejects
|
|
909
|
+
// any media message or ordered media part by its type.
|
|
910
|
+
if (history !== undefined) {
|
|
911
|
+
params.history = JSON.stringify(history.map((message) => {
|
|
912
|
+
if (message?.type === "media")
|
|
913
|
+
return { ...message, content: "" };
|
|
914
|
+
const textMessage = message;
|
|
915
|
+
if (Array.isArray(textMessage?.content)) {
|
|
916
|
+
return {
|
|
917
|
+
...message,
|
|
918
|
+
content: textMessage.content.map((part) => isByteMediaPart(part) ? { type: "media" } : part),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return message;
|
|
922
|
+
}));
|
|
923
|
+
}
|
|
924
|
+
if (tools !== undefined)
|
|
925
|
+
params.tools = JSON.stringify(tools);
|
|
926
|
+
return this.addon.calcTokens(params);
|
|
927
|
+
}
|
|
829
928
|
getState() {
|
|
830
929
|
return this.state;
|
|
831
930
|
}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|