anyformer 0.0.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/README.md +107 -17
- package/SPEC.md +68 -0
- package/index.js +17 -0
- package/package.json +41 -6
- package/src/backends/diskformer-llama.js +161 -0
- package/src/backends/diskformer.js +124 -0
- package/src/backends/kohra.js +59 -0
- package/src/backends/localmind.js +116 -0
- package/src/backends/transformers.js +191 -0
- package/src/backends/wllama.js +122 -0
- package/src/describe.js +47 -0
- package/src/errors.js +36 -0
- package/src/gguf-split.js +160 -0
- package/src/load.js +127 -0
- package/src/models.js +50 -0
- package/src/pipeline.js +27 -0
- package/src/probe.js +37 -0
- package/src/resolve.js +49 -0
- package/src/sources.js +23 -0
- package/src/store.js +145 -0
- package/src/tools.js +148 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.1 — 2026-10-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- MiniCPM-V 4.7 35B-A3B (openbmb/MiniCPM-V-4.7-35B-A3B-gguf, Q4_0, 19.7 GB) on the `diskformer` back end, through
|
|
7
|
+
diskformer's `MiniCpmV47Ssd`: text, and images through `ensureVision()` and `{ type: 'image', rgb, width, height }`
|
|
8
|
+
blocks. Floor 4 GB (Qwen3.6's; the smallest working budget is unmeasured); 14.1 tok/s over short continuations at a
|
|
9
|
+
4 GiB pool, ~6.1 GB on the GPU (LocalMind's gate). It ranks before `diskformer-llama`, which has no image path.
|
|
10
|
+
Not yet run through anyformer in a browser; LocalMind's live check is that check.
|
|
11
|
+
|
|
12
|
+
## 0.3.0 — 2026-10-23
|
|
13
|
+
|
|
14
|
+
What LocalMind's worker uses from its disk-tier engines, so its models can move onto anyformer with nothing lost.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `generate()` chunks on the `diskformer` back end carry `token`, the engine's token id.
|
|
18
|
+
- `session.think`: `{ openId, closeId, inPrompt, preamble, always }`, where a model's reasoning starts and ends as
|
|
19
|
+
token ids (Gemma 4's thought channel, Qwen3.6 and Ling's `<think>`); null where the back end has none.
|
|
20
|
+
- `session.prime(system)`, where the engine has it (Qwen3.6 35B): checkpoints a system prompt after load.
|
|
21
|
+
- `session.ensureVision({ onProgress })`, where the engine has it; image blocks `{ type: 'image', rgb, width, height }`
|
|
22
|
+
pass through to such an engine. No vision model is in `DISKFORMER_MODELS` yet.
|
|
23
|
+
- `load(repo, { poolBytes })`: the expert cache size, passed to the engine; the budget then only admits the model.
|
|
24
|
+
- `load(repo, { localFile })`: a GGUF the user picked, ingested instead of downloaded.
|
|
25
|
+
|
|
26
|
+
## 0.2.5 — 2026-10-23
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- diskformer pinned to v0.6.3 (1d8b418): Ling 3.0 Flash loads a complete store without reading the parts' headers
|
|
30
|
+
from Hugging Face; the browser llama.cpp build is rebuilt (split GGUFs still refused there). Granite 4.0 H Tiny
|
|
31
|
+
through `diskformer-llama` on the new build: 9/9, chat path = reference. The hand engines anyformer gated are
|
|
32
|
+
unchanged (`qwen3_moe_ssd.js` only adds an export).
|
|
33
|
+
|
|
34
|
+
## 0.2.4 — 2026-10-23
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- The `diskformer` back end's `generate()` honours `thinking`, as `diskformer-llama` does since 0.2.1: off by default,
|
|
38
|
+
`{ thinking: true }` lets Gemma 4, Qwen3.6 or Ling 3.0 Flash reason first (the engines' `enableThinking`).
|
|
39
|
+
|
|
40
|
+
## 0.2.3 — 2026-10-23
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- Ling 3.0 Flash (inclusionAI/Ling-3.0-flash-GGUF, two-part Q4_K_M, 77 GB) on the `diskformer` back end, through
|
|
44
|
+
diskformer's `LingMoeSsd` hand engine: from a 6.8 GB budget, 5.53 tok/s and an 80.7 GB OPFS store (LocalMind's gate).
|
|
45
|
+
Not yet run through anyformer in a browser: LocalMind's live check through anyformer is that check.
|
|
46
|
+
- `describe()` keeps the Hub's last answer per repo in Cache Storage (`anyformer-describe-v1`) and uses it when the
|
|
47
|
+
network fails, so a downloaded model loads offline.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- diskformer pinned to v0.6.2 (f5f1177): adds `ling_moe_ssd.js` and two quant formats to `iq_quants.js`, which no
|
|
51
|
+
engine gated earlier imports; the earlier gates stand.
|
|
52
|
+
|
|
53
|
+
## 0.2.2 — 2026-10-23
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
- `load(repo, { file })` and `plan(repo, { file })`: load a chosen GGUF from the repo; a back end that cannot load
|
|
57
|
+
that file refuses with code `model`.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
- `diskformer-llama` loads the file diskformer checked when the repo has it (`CHECKED_FILES`, from diskformer's
|
|
61
|
+
catalog and coverage table), else the preferred quant. GLM-4.7-Flash now defaults to Q3_K_M (14.6 GB, gated 8/9 by
|
|
62
|
+
diskformer), not Q4_K_M (18.3 GB, not gated). Granite 4.0 H Tiny and Gemma 4 26B keep the files their gates used.
|
|
63
|
+
|
|
64
|
+
## 0.2.1 — 2026-10-23
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
- diskformer pinned to v0.6.1 (5bccf32): its llama.cpp build takes a thinking switch. `diskformer-llama`'s
|
|
68
|
+
`generate()` now turns thinking off by default (`thinking: true` turns it on), so a model that reasons first answers
|
|
69
|
+
directly, as on the other back ends.
|
|
70
|
+
|
|
71
|
+
### Measured (Chrome, M4 Pro; correctness only)
|
|
72
|
+
- Granite 4.0 H Tiny via `diskformer-llama`: 9/9; the chat path matches the reference.
|
|
73
|
+
- Gemma 4 26B-A4B via `diskformer-llama`: 8/9 (miss: system-pirate, as diskformer's own run); the chat path matches the
|
|
74
|
+
reference, with no thought block. The hand engines are unchanged in v0.6.1, so their 9/9 at 0.2.0 stands.
|
|
75
|
+
|
|
76
|
+
## 0.2.0 — 2026-10-22
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
- `diskformer-llama` back end: mixture-of-experts GGUFs through diskformer's llama.cpp path (`diskformer/llama`,
|
|
80
|
+
llama.cpp with WebGPU in a worker, experts paged from OPFS), for the 17 architectures diskformer has checked
|
|
81
|
+
(`LLAMA_ARCHITECTURES`). A diskformer hand-written engine still ranks first where it covers the model.
|
|
82
|
+
- `probe()` reports `jspi` (WebAssembly JSPI), which the llama.cpp build needs.
|
|
83
|
+
- `stored()` and `remove()` cover the llama.cpp path's GGUFs in OPFS (`gguf/`).
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
- diskformer pinned to v0.6.0 (439ad4e) for both its hand engines and the llama.cpp path.
|
|
87
|
+
|
|
88
|
+
### Measured (Chrome, M4 Pro)
|
|
89
|
+
- Granite 4.0 H Tiny through `diskformer-llama`: 9/9 replies identical to diskformer's reference
|
|
90
|
+
(`npm run test:browser -- llama-gate`).
|
|
91
|
+
- Gemma 4 26B-A4B through `diskformer-llama`: 8/9 identical to llama.cpp b9830, the same 8/9 (miss: system-pirate)
|
|
92
|
+
diskformer records for its own Chrome run. The resolver still sends this repo to the hand engine.
|
|
93
|
+
- Gemma 4 26B-A4B on the hand engine at the new pin: 9/9 identical (`diskformer-chat`).
|
|
94
|
+
|
|
95
|
+
### Known gaps
|
|
96
|
+
- `diskformer-llama`'s chat path does not turn thinking off: a model that reasons first (Gemma 4, Qwen3.6, gpt-oss)
|
|
97
|
+
begins its reply with its thought block. Closed in 0.2.1.
|
|
98
|
+
|
|
99
|
+
## 0.1.0 — 2026-10-14
|
|
100
|
+
|
|
101
|
+
The first release with code. `anyformer@0.0.1` reserved the name and held none.
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
- `plan(repo)` and `load(repo)`: read the Hub repo, probe the browser, ask every back end, run the best yes. A refusal
|
|
105
|
+
carries a closed code (`format`, `model`, `task`, `webgpu`, `f16`, `opfs`, `budget`, `threw`) and one remedy in `REFUSALS`.
|
|
106
|
+
- Five back ends:
|
|
107
|
+
- `transformers`: transformers.js 4.3.1. Text generation, image and audio chat (Gemma 4, Qwen3.5), embeddings,
|
|
108
|
+
speech to text, detection, classification, depth, segmentation.
|
|
109
|
+
- `kohra`: text diffusion (MDLM, BD3LM) over onnxruntime-web.
|
|
110
|
+
- `localmind`: LocalMind's hand-written WebGPU kernels (LFM2 / LFM2.5, Gemma 4 E2B / E4B, Ternary Bonsai 2).
|
|
111
|
+
- `wllama`: llama.cpp in WebAssembly for any other GGUF up to about 3.6 GB, on WebGPU or the CPU; files over 2 GB
|
|
112
|
+
split into OPFS shards once.
|
|
113
|
+
- `diskformer`: weights in OPFS, only what a step needs on the GPU (Gemma 4 26B-A4B from 2.5 GB, Qwen3.6 35B-A3B from 4 GB).
|
|
114
|
+
- `MODELS`: models that pass a browser test, loadable by entry.
|
|
115
|
+
- `pipeline(task, repo)`: transformers.js's call shape over any back end.
|
|
116
|
+
- `stored()` and `remove(repo)`: what is downloaded in this browser, across every back end.
|
|
117
|
+
- Coded errors: `AnyformerError` with a code from `ERROR_CODES`, checked with `isAnyformerError(e, code)`.
|
|
118
|
+
- One `generate()` at a time per session, in call order.
|
|
119
|
+
- Options: `load()` takes `task`, `maxCtx`, `dtype` and `signal`; `generate()` takes `maxNewTokens`, `repetitionPenalty`
|
|
120
|
+
and `signal`; each back end's `.source` (for example `kohraBackend.source`) overrides where its runtime loads from.
|
|
121
|
+
- Pinned runtimes in `src/sources.js` (exact versions and commits); `npm run pin` moves them.
|
|
122
|
+
- Agent face: 9 tools over one manifest, through WebMCP's `navigator.modelContext` or `window.anyformer.tools`.
|
|
123
|
+
- The workbench (`examples/chat/`): pick a model, see the plan, then chat, compare embeddings or detect objects.
|
|
124
|
+
|
|
125
|
+
### Measured (Chrome, M4 Pro, 24 GB)
|
|
126
|
+
- 13 of 13 LocalMind Hub models plan at an 8 GB budget; 12 ran in a browser test.
|
|
127
|
+
- 10 of 10 replies byte-identical with LocalMind direct on the same transformers.js pin.
|
|
128
|
+
- Speed 0.99–1.02× LocalMind direct on the steady back ends.
|
|
129
|
+
|
|
130
|
+
### Not tested yet
|
|
131
|
+
Audio input blocks, abort mid-reply, speech / classification / depth / segmentation in a browser, any machine other
|
|
132
|
+
than the M4 Pro, and Qwen3.6 35B-A3B (it needs more than this machine's 24 GB).
|
package/README.md
CHANGED
|
@@ -1,30 +1,120 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">anyformer.js</h1>
|
|
2
2
|
|
|
3
|
-
One call to run
|
|
4
|
-
GPU budget
|
|
3
|
+
**One call to run a model in a browser tab: anyformer reads the repo, probes the browser, and picks the back end that
|
|
4
|
+
fits the model's format, architecture and your GPU budget.**
|
|
5
|
+
|
|
6
|
+
Plain ES modules, no build step, no dependencies to install. Chromium with WebGPU (GGUF models also run on the CPU). Weights stay in the user's own
|
|
7
|
+
browser storage; no account, no server, no telemetry.
|
|
8
|
+
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](package.json)
|
|
5
11
|
|
|
6
12
|
## Install
|
|
7
13
|
|
|
8
|
-
|
|
14
|
+
| How | Command |
|
|
15
|
+
|---|---|
|
|
16
|
+
| npm | `npm install anyformer` |
|
|
17
|
+
| CDN | `import { load } from 'https://cdn.jsdelivr.net/npm/anyformer@0.1.0/+esm';` |
|
|
18
|
+
| Copy | `index.js` and `src/`; nothing else is needed |
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { load } from 'anyformer';
|
|
22
|
+
|
|
23
|
+
const chat = await load('onnx-community/Qwen2.5-0.5B-Instruct', { budget: 2.5e9, onProgress: console.log });
|
|
24
|
+
for await (const { text } of chat.generate([{ role: 'user', content: 'What does a CPU do?' }], { maxNewTokens: 64 }))
|
|
25
|
+
show(text);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Every session has `run(input)`: chat messages for text generation (content a string, or blocks such as
|
|
29
|
+
`[{ type: 'image', image }, { type: 'text', text }]` for Gemma 4 and Qwen3.5), a string or strings for embeddings, ImageData for
|
|
30
|
+
vision, 16 kHz samples for speech. `load(repo, { task })` picks a task when a repo has more than one. The first load
|
|
31
|
+
downloads the weights into the browser; later loads read them from there. Each back end's runtime loads
|
|
32
|
+
from jsDelivr only when that back end is chosen.
|
|
33
|
+
|
|
34
|
+
## Try it
|
|
35
|
+
|
|
36
|
+
Serve the repo and open the workbench: pick a model, see which back end anyformer chose and why every other one
|
|
37
|
+
refused, then chat (with an image for the vision models), compare embeddings, or detect objects; remove downloads.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python3 -m http.server 8000 # then open http://localhost:8000/examples/chat/
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Why
|
|
44
|
+
|
|
45
|
+
You have a model id and a browser tab. Whether it runs depends on its format (ONNX, GGUF), its architecture, whether it
|
|
46
|
+
is autoregressive or diffusion, and how much GPU memory the tab can take. Each runtime covers part of that, and you
|
|
47
|
+
find out which part by failing to load.
|
|
48
|
+
|
|
49
|
+
anyformer asks every back end first. `plan(repo)` returns the model's description, the browser's capabilities and each
|
|
50
|
+
back end's answer: yes with an estimate (GPU bytes, disk bytes, tok/s when measured), or no with a code and a reason.
|
|
51
|
+
`load(repo)` runs the best yes.
|
|
52
|
+
|
|
53
|
+
## Back ends
|
|
54
|
+
|
|
55
|
+
| Back end | Runs | Models |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `transformers` | transformers.js 4.3.1: text generation, image and audio chat (Gemma 4, Qwen3.5), embeddings, speech to text, detection, classification, depth, segmentation | ONNX repos in the onnx-community / Xenova layout |
|
|
58
|
+
| `kohra` | kohra, text-diffusion over onnxruntime-web | MDLM and BD3LM repos tagged `text-diffusion` |
|
|
59
|
+
| `localmind` | LocalMind's hand-written WebGPU kernels, the whole model on the GPU | any LFM2 / LFM2.5 GGUF; Gemma 4 E2B / E4B QAT-mobile; Ternary Bonsai 2 |
|
|
60
|
+
| `wllama` | llama.cpp in WebAssembly: WebGPU when the budget holds the model, the CPU otherwise (no WebGPU needed) | any GGUF up to about 3.6 GB that no engine above covers; files over 2 GB are split into OPFS shards once |
|
|
61
|
+
| `diskformer` | diskformer engines: weights in OPFS, only what a step needs on the GPU | Gemma 4 26B-A4B (from 2.5 GB), Qwen3.6 35B-A3B (from 4 GB), Ling 3.0 Flash 124B (from 6.8 GB), MiniCPM-V 4.7 with images (from 4 GB) |
|
|
62
|
+
| `diskformer-llama` | diskformer's llama.cpp build with WebGPU, mixture-of-experts layers paged in from OPFS | GGUFs of the 17 MoE architectures diskformer has checked (Granite 4.0 H, gpt-oss, GLM-4.7-Flash, LFM2 MoE…); a hand engine above wins where it covers the model |
|
|
63
|
+
|
|
64
|
+
Each runtime loads from an exact version or commit in [src/sources.js](src/sources.js); set a back end's `.source`
|
|
65
|
+
(for example `kohraBackend.source`) to load another copy. `BACKENDS` lists the back ends `load()` asks by default: each joins after its browser test passes.
|
|
66
|
+
|
|
67
|
+
## When no back end can run it
|
|
68
|
+
|
|
69
|
+
`load()` throws an `AnyformerError` with code `NO_BACKEND` and the plan, naming each back end, its code and its
|
|
70
|
+
reason. Codes are a closed set, each with one remedy in `REFUSALS`: `format`, `model`, `task`, `webgpu`, `f16`, `opfs`, `budget` (with `need`, the budget that would pass)
|
|
71
|
+
and `threw`. Every other failure is an `AnyformerError` too, with a code from `ERROR_CODES` (`LOAD_FAILED`,
|
|
72
|
+
`BUDGET_TOO_SMALL`, `ABORTED`, `EXECUTION_FAILED`, `INVALID_ARGUMENT`, `DISPOSED`); check it with
|
|
73
|
+
`isAnyformerError(e, code)`. A session runs one `generate()` at a time, in call order.
|
|
9
74
|
|
|
10
|
-
##
|
|
75
|
+
## Commands
|
|
11
76
|
|
|
12
|
-
|
|
13
|
-
|
|
77
|
+
```js
|
|
78
|
+
plan(repo, { budget, task }) // what would run it, and why the others would not; loads nothing
|
|
79
|
+
load(repo, { budget, task, file, maxCtx, poolBytes, localFile, onProgress, signal }) // → { backend, task, plan, run(input), generate?, think?, prime?, dispose() }
|
|
80
|
+
pipeline(task, repo, { budget }) // transformers.js's call shape: gen(messages) → [{ generated_text }]
|
|
81
|
+
probe() // WebGPU, shader-f16, subgroups, limits, OPFS quota, memory64
|
|
82
|
+
describe(repo) // Hub repo → formats, architecture, task, tags, files
|
|
83
|
+
resolve(model, caps, backends, { budget }) // rank back ends for a described model
|
|
84
|
+
stored() · remove(repo) // what is downloaded in this browser; delete one
|
|
85
|
+
MODELS.chat.DEFAULT // models that pass a browser test; load(MODELS.chat.GEMMA4_26B_A4B) works too
|
|
86
|
+
registerTools() // publish the agent face (below)
|
|
87
|
+
```
|
|
14
88
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
89
|
+
Agents get the same calls as tools (`anyformer_models`, `_probe`, `_plan`, `_load`, `_generate`, `_sessions`, `_unload`,
|
|
90
|
+
`_stored`, `_remove`) through
|
|
91
|
+
WebMCP's `navigator.modelContext` where the browser has it and `window.anyformer.tools` otherwise; `anyformer_load`
|
|
92
|
+
loads nothing until the caller accepts the plan's disk bytes, and `anyformer_remove` deletes nothing without
|
|
93
|
+
`confirm: true`.
|
|
19
94
|
|
|
20
|
-
|
|
95
|
+
## Verify it yourself
|
|
21
96
|
|
|
22
|
-
|
|
97
|
+
```bash
|
|
98
|
+
npm test # Node: probe, describe, resolver, back-end verdicts, agent face
|
|
99
|
+
npm run test:browser -- probe # headless Chrome: WebGPU, f16, OPFS
|
|
100
|
+
npm run test:browser -- transformers # Qwen2.5-0.5B answers on topic
|
|
101
|
+
npm run test:browser -- kohra # Qwen3-0.6B MDLM answers on topic
|
|
102
|
+
npm run test:browser -- embed # MiniLM embeddings rank a paraphrase above an unrelated sentence
|
|
103
|
+
npm run test:browser -- detect # DETR finds both cats in cats.jpg
|
|
104
|
+
npm run test:browser -- examples/chat/index.html --query "auto=1" # the workbench loads and answers
|
|
105
|
+
npm run test:browser -- pipeline --port 8192 --profile ~/.cache/anyformer-chat-profile # pipeline() on a GGUF
|
|
106
|
+
npm run test:browser -- store # list a download, remove it
|
|
107
|
+
npm run test:browser -- chat --query "repo=LiquidAI/LFM2.5-1.2B-Instruct-GGUF" # a LocalMind engine
|
|
108
|
+
npm run test:browser -- vision --query "repo=onnx-community/Qwen3.5-4B-ONNX-OPT" # names the two cats
|
|
109
|
+
npm run test:browser -- diskformer-chat # Gemma 4 26B-A4B at 2.5 GB: 9/9 replies identical to llama.cpp b9830
|
|
110
|
+
npm run test:browser -- llama-gate --query "repo=ibm-granite/granite-4.0-h-tiny-GGUF&refs=diskformer-granite-refs" # 9/9 via diskformer-llama
|
|
111
|
+
```
|
|
23
112
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
113
|
+
The browser tests fail a reply that is off topic or repeats itself, and `diskformer-chat` fails any reply that differs
|
|
114
|
+
from llama.cpp's by one character. Only Apple silicon (M4 Pro, Chrome) has run them. Not tested yet: audio input blocks, abort mid-reply, speech / classification / depth / segmentation in a browser, any
|
|
115
|
+
machine other than the M4 Pro, and Qwen3.6 35B-A3B (it needs more than that machine's 24 GB).
|
|
27
116
|
|
|
28
117
|
## License
|
|
29
118
|
|
|
30
|
-
MIT
|
|
119
|
+
MIT. Spec and agent contract: [SPEC.md](SPEC.md) · depends on [diskformer.js](https://github.com/NakliTechie/diskformer.js)
|
|
120
|
+
· [kohra](https://github.com/NakliTechie/kohra) · [LocalMind](https://github.com/NakliTechie/LocalMind)
|
package/SPEC.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# anyformer.js — spec
|
|
2
|
+
|
|
3
|
+
Tier: Product (several back ends, a resolver, one store). anyformer depends on diskformer and kohra; neither depends on
|
|
4
|
+
anyformer.
|
|
5
|
+
|
|
6
|
+
## §0 Agent contract (DRIVER v0.1 pass, 2026-10-08)
|
|
7
|
+
|
|
8
|
+
The driver: an agent in a browser tab (or a script) that must run a model it names by repo id, with no idea which
|
|
9
|
+
runtime fits, how much GPU memory the tab may take, or whether a download is 0.5 GB or 36 GB. It is context-poor, may
|
|
10
|
+
be killed mid-turn, and must not burn a user's disk or bandwidth by guessing.
|
|
11
|
+
|
|
12
|
+
1. **One perception act: `anyformer_plan` (`plan()`).** One call returns the model's formats, architecture and task,
|
|
13
|
+
the browser's caps, the budget, the chosen back end, and every back end's verdict. The agent never probes or
|
|
14
|
+
fetches separately before deciding.
|
|
15
|
+
2. **Closed vocabularies.** Errors from load() and a session are `AnyformerError` with `code` in `ERROR_CODES`
|
|
16
|
+
(NO_BACKEND · LOAD_FAILED · BUDGET_TOO_SMALL+need · ABORTED · EXECUTION_FAILED · INVALID_ARGUMENT · DISPOSED),
|
|
17
|
+
checked by name and code (`isAnyformerError`), so two copies of the library agree. A refusal is `{code, reason, need?}` with `code` in `REFUSALS`: format · model · task · webgpu ·
|
|
18
|
+
f16 · opfs · budget · threw. Each code has one remedy (`REFUSALS[code]`); `budget` carries `need`, the bytes that
|
|
19
|
+
would pass. Tool consequences are `read` · `stages` · `mutates`. Tool errors are `{error}` data, never throws.
|
|
20
|
+
3. **One verdict per next action.** `chosen` set → load. `chosen` null → read the codes: format/model → another repo;
|
|
21
|
+
webgpu/f16/opfs → another browser or GPU (nothing in this tab fixes it); budget → re-plan with `need`; threw → a bug.
|
|
22
|
+
4. **Bounded output.** `anyformer_plan` grows with the number of back ends, never with the repo's file count (the file
|
|
23
|
+
list stays out). The journal holds the last 50 calls. `generate` returns one reply.
|
|
24
|
+
5. **Every failure names its remedy.** NO_BACKEND lists each back end with its code and reason; `REFUSALS[code]`
|
|
25
|
+
is the next move.
|
|
26
|
+
6. **Crash-safe and idempotent.** A session runs one generate at a time; dispose() waits for it and is idempotent. `plan` and `probe` have no side effects. Weights land in OPFS through diskformer's
|
|
27
|
+
resumable ingest (an interrupted download resumes); transformers.js and kohra use the browser HTTP cache. Loading
|
|
28
|
+
twice loads twice; it never corrupts a store.
|
|
29
|
+
7. **The tool holds the memory.** `anyformer_sessions` renders the open sessions and the last 50 tool calls with the
|
|
30
|
+
door each came through (`direct`, `window`, `modelContext`).
|
|
31
|
+
8. **Accretion.** A back end joins `BACKENDS` only after its browser test passes (test/registry.test.js pins the list);
|
|
32
|
+
every escaped wrong answer becomes a browser-test check (the transformers.js q4f16 word salad became `onTopic`).
|
|
33
|
+
9. **The tower.** One line per layer; each reads only the one below.
|
|
34
|
+
- `probe` (browser caps) · `describe` (Hub repo → descriptor)
|
|
35
|
+
- back ends: `canRun` (pure, descriptor + caps) · `load` → session
|
|
36
|
+
- `resolve` (rank the verdicts) → `plan` (probe + describe + resolve) → `load` (plan + back-end load)
|
|
37
|
+
- agent face: `TOOLS` manifest → `call` → WebMCP `registerTool` / `window.anyformer.tools`
|
|
38
|
+
10. **The evaluator stays outside.** The browser tests (test/browser/) judge back ends against fixed references:
|
|
39
|
+
llama.cpp b9830 replies for diskformer, an on-topic check for transformers.js and kohra. A page that times out
|
|
40
|
+
exits 2 (indeterminate), never 0. Staging is enforced in the tool, not left to the agent: `anyformer_load` loads
|
|
41
|
+
nothing unless `acceptDiskBytes` covers the plan's disk bytes.
|
|
42
|
+
|
|
43
|
+
Self-check: perception act ✓ · closed vocabularies ✓ · output bound ✓ · crash-safety ✓ · trajectory ✓ · accretion ✓ ·
|
|
44
|
+
tower ✓ · evaluator boundary ✓.
|
|
45
|
+
|
|
46
|
+
## What shipped (Batch A)
|
|
47
|
+
|
|
48
|
+
- `load(repo, {budget, maxCtx, backends, onProgress, signal})` → `{backend, plan, generate(messages, {maxNewTokens, signal}),
|
|
49
|
+
dispose()}`; `generate` yields `{text, tokens}`, `text` cumulative.
|
|
50
|
+
- Tasks: `plan/load(repo, { task })`, default the repo's pipeline tag; every session has `run(input)`, text generation
|
|
51
|
+
also `generate()`. Back ends list their `tasks`.
|
|
52
|
+
- Back ends: transformers.js 4.3.1 (ONNX text-generation, q4 first), kohra @main (text-diffusion ONNX), diskformer
|
|
53
|
+
@main (pinned Gemma 4 26B-A4B and Qwen3.6 35B-A3B GGUFs). transformers.js also runs feature-extraction, speech recognition, object
|
|
54
|
+
detection, image classification, depth, segmentation, token and text classification on fp32 graphs. kohra and diskformer load from their main branches;
|
|
55
|
+
`<backend>.source` pins another copy.
|
|
56
|
+
- transformers.js also runs Gemma 4 and Qwen3.5 ONNX as chat with image (and, for Gemma 4, audio) blocks, through
|
|
57
|
+
their conditional-generation classes at q4f16, as LocalMind does.
|
|
58
|
+
- localmind @main: LocalMind's hand-written engines, matched by architecture and format (lfm2 GGUF, Gemma 4
|
|
59
|
+
QAT-mobile, qwen35 Prism ternary); resident on the GPU, so budget ≥ file size.
|
|
60
|
+
- wllama 3.4.1 (LocalMind's pin): any GGUF ≤ ~3.6 GB no architecture engine covers (a repo's split set as is; a
|
|
61
|
+
single file over 2 GB split once into OPFS shards, src/gguf-split.js); WebGPU when the budget
|
|
62
|
+
holds it, CPU otherwise. Thinking off (chat_template_kwargs). Ranks last.
|
|
63
|
+
- Pins: src/sources.js holds exact versions and commits; `npm run pin` moves the git pins to each repo's main.
|
|
64
|
+
- Agent face: 9 tools (models, probe, plan, load, generate, sessions, unload, stored, remove). Person-only acts: none.
|
|
65
|
+
- `MODELS`: tested entries by family; `load(entry)`. `stored()` / `remove(repo)` over the transformers.js cache and
|
|
66
|
+
diskformer's OPFS stores.
|
|
67
|
+
- Gaps: no wasm fallback (every back end needs WebGPU); one store per back end, not the shared OPFS store yet (kohra's graph is in the HTTP cache, unlisted);
|
|
68
|
+
llama.cpp-webgpu not started; speed estimates exist for diskformer only.
|
package/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// anyformer — one call to run any model in a browser tab. Plain ESM, no build step.
|
|
2
|
+
export { load, plan, BACKENDS, DEFAULT_BUDGET } from './src/load.js';
|
|
3
|
+
export { AnyformerError, isAnyformerError, ERROR_CODES } from './src/errors.js';
|
|
4
|
+
export { probe, memory64 } from './src/probe.js';
|
|
5
|
+
export { describe, fromHubInfo } from './src/describe.js';
|
|
6
|
+
export { resolve, REFUSALS } from './src/resolve.js';
|
|
7
|
+
export { transformersBackend } from './src/backends/transformers.js';
|
|
8
|
+
export { kohraBackend } from './src/backends/kohra.js';
|
|
9
|
+
export { diskformerBackend, DISKFORMER_MODELS } from './src/backends/diskformer.js';
|
|
10
|
+
export { diskformerLlamaBackend, LLAMA_ARCHITECTURES } from './src/backends/diskformer-llama.js';
|
|
11
|
+
export { TOOLS, call, registerTools, journal, toolOptions } from './src/tools.js';
|
|
12
|
+
export { MODELS } from './src/models.js';
|
|
13
|
+
export { stored, remove } from './src/store.js';
|
|
14
|
+
export { pipeline } from './src/pipeline.js';
|
|
15
|
+
export { localmindBackend } from './src/backends/localmind.js';
|
|
16
|
+
export { wllamaBackend } from './src/backends/wllama.js';
|
|
17
|
+
export { PINS, SOURCES } from './src/sources.js';
|
package/package.json
CHANGED
|
@@ -1,13 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyformer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "One call to run
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "One call to run a model in a browser tab: anyformer reads the Hub repo, probes the browser, and picks the back end (transformers.js, LocalMind's WebGPU kernels, diskformer, kohra) that fits the model's format, architecture and your GPU budget.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"src/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"SPEC.md"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test 'test/*.test.js'",
|
|
21
|
+
"test:browser": "node test/browser/run.mjs",
|
|
22
|
+
"pin": "node scripts/pin-sources.mjs"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"agent",
|
|
26
|
+
"browser",
|
|
27
|
+
"diffusion",
|
|
28
|
+
"diskformer",
|
|
29
|
+
"gguf",
|
|
30
|
+
"inference",
|
|
31
|
+
"llm",
|
|
32
|
+
"localmind",
|
|
33
|
+
"onnx",
|
|
34
|
+
"transformers",
|
|
35
|
+
"webgpu",
|
|
36
|
+
"webmcp"
|
|
37
|
+
],
|
|
6
38
|
"license": "MIT",
|
|
7
39
|
"author": "Chirag Patnaik",
|
|
8
40
|
"homepage": "https://github.com/NakliTechie/anyformer.js#readme",
|
|
9
|
-
"repository": {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/NakliTechie/anyformer.js.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/NakliTechie/anyformer.js/issues"
|
|
47
|
+
}
|
|
13
48
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// diskformer-llama: mixture-of-experts GGUFs through diskformer's llama.cpp path (diskformer/llama: llama.cpp with
|
|
2
|
+
// WebGPU in a worker, the experts paged in from OPFS on demand). One engine for every architecture diskformer has
|
|
3
|
+
// checked in Chrome, so a new MoE model of a covered architecture needs no anyformer code. Where diskformer also has a
|
|
4
|
+
// hand-written engine for the model (the diskformer back end), that one carries a measured speed and ranks first.
|
|
5
|
+
// llama.js loads from the CDN at load() time, at the commit src/sources.js pins; set diskformerLlamaBackend.source to
|
|
6
|
+
// load another copy.
|
|
7
|
+
|
|
8
|
+
import { AnyformerError } from '../errors.js';
|
|
9
|
+
import { SOURCES } from '../sources.js';
|
|
10
|
+
|
|
11
|
+
// Architectures that pass diskformer's two checks (docs/coverage.md at the pinned commit): paging is exact natively,
|
|
12
|
+
// and the Chrome replies are compared with llama.cpp. Other models of the same architecture are expected to run.
|
|
13
|
+
// Left out: hy_v3 and afmoe (run, below diskformer's bar), laguna (unknown tokens), dense architectures (no experts
|
|
14
|
+
// to page), and Mixtral-style `llama` MoE (runs, too slow to use).
|
|
15
|
+
export const LLAMA_ARCHITECTURES = [
|
|
16
|
+
'gemma4', 'qwen3moe', 'qwen3next', 'qwen35moe', 'qwen3vlmoe', 'deepseek2', 'nemotron_h_moe', 'ernie4_5-moe',
|
|
17
|
+
'gpt-oss', 'cohere2moe', 'lfm2moe', 'mellum', 'bailingmoe2', 'granitehybrid', 'bailingmoe3', 'flint', 'olmoe',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Quant preference by the token in the file name; the first one the repo has wins. The files diskformer checked are
|
|
21
|
+
// Q4_K_M, Q4_0, Q4_1, MXFP4 and Q3_K_M.
|
|
22
|
+
const QUANTS = ['Q4_K_M', 'Q4_0', 'Q4_1', 'MXFP4', 'Q3_K_M', 'Q4_K_S', 'Q5_K_M', 'Q5_K_S', 'Q6_K', 'Q8_0'];
|
|
23
|
+
|
|
24
|
+
// The exact files diskformer checked (ggml/chat.html CATALOG and docs/coverage.md at the pinned commit). When a repo
|
|
25
|
+
// holds one, it is the default: the coverage numbers were measured on that file.
|
|
26
|
+
export const CHECKED_FILES = [
|
|
27
|
+
'gemma-4-26B_q4_0-it.gguf', 'Qwen3-30B-A3B-Instruct-2507-Q3_K_M.gguf', 'Qwen3-Next-80B-A3B-Instruct-Q4_K_M.gguf',
|
|
28
|
+
'Qwen3.6-35B-A3B-UD-Q3_K_M.gguf', 'Qwen3-VL-30B-A3B-Instruct-Q3_K_M.gguf', 'GLM-4.7-Flash-Q3_K_M.gguf',
|
|
29
|
+
'NVIDIA-Nemotron-3-Nano-30B-A3B-Q4_K_M.gguf', 'ERNIE-4.5-21B-A3B-Thinking-Q4_K_M.gguf', 'gpt-oss-20b-MXFP4.gguf',
|
|
30
|
+
'North-Mini-Code-1.0-UD-Q3_K_M.gguf', 'LFM2.5-24B-A2B-QAD-Q4_0.gguf', 'LFM2-8B-A1B-Q4_K_M.gguf',
|
|
31
|
+
'Mellum2-12B-A2.5B-Instruct-Q4_K_M.gguf', 'Ling-mini-2.0-Q4_K_M.gguf', 'granite-4.0-h-tiny-Q4_K_M.gguf',
|
|
32
|
+
'Ling-3.0-tiny-Q4_K_M.gguf', 'flint-int4-q4_1.gguf', 'OLMoE-1B-7B-0125-Instruct-Q4_K_M.gguf',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// The GGUF to load: the language model (no mmproj vision tower), one file (no split sets). The caller's file when
|
|
36
|
+
// given; else the file diskformer checked; else the preferred quant.
|
|
37
|
+
export function pickGguf(model, file = null) {
|
|
38
|
+
const lms = model.files.gguf.filter((f) => !/mmproj/i.test(f.name) && !/-\d{5}-of-\d{5}\.gguf$/.test(f.name));
|
|
39
|
+
if (file) return lms.find((f) => f.name === file) || null;
|
|
40
|
+
const checked = lms.find((f) => CHECKED_FILES.includes(f.name.split('/').pop()));
|
|
41
|
+
if (checked) return checked;
|
|
42
|
+
for (const q of QUANTS) {
|
|
43
|
+
const f = lms.find((x) => new RegExp(`(^|[-_.])${q}([-_.]|$)`, 'i').test(x.name.replace(/\.gguf$/i, '')));
|
|
44
|
+
if (f) return f;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// OPFS keeps llama.cpp's GGUFs by file name (gguf/<name>), with no repo. anyformer records name → repo here, so
|
|
50
|
+
// stored() can say which repo a file belongs to.
|
|
51
|
+
export const LLAMA_INDEX = 'anyformer-llama.json';
|
|
52
|
+
async function readIndex() {
|
|
53
|
+
try { return JSON.parse(await (await (await (await navigator.storage.getDirectory()).getFileHandle(LLAMA_INDEX)).getFile()).text()); } catch { return {}; }
|
|
54
|
+
}
|
|
55
|
+
async function writeIndex(index) {
|
|
56
|
+
const h = await (await navigator.storage.getDirectory()).getFileHandle(LLAMA_INDEX, { create: true });
|
|
57
|
+
const w = await h.createWritable();
|
|
58
|
+
await w.write(JSON.stringify(index));
|
|
59
|
+
await w.close();
|
|
60
|
+
}
|
|
61
|
+
export { readIndex as readLlamaIndex };
|
|
62
|
+
|
|
63
|
+
// Chat content: a string, or text blocks joined. llama.cpp's chat here takes text only.
|
|
64
|
+
function textOnly(messages) {
|
|
65
|
+
return messages.map((m) => {
|
|
66
|
+
if (typeof m.content === 'string') return m;
|
|
67
|
+
const other = m.content.find((b) => b.type !== 'text');
|
|
68
|
+
if (other) throw new AnyformerError('INVALID_ARGUMENT', `diskformer-llama runs text only; got a ${other.type} block`);
|
|
69
|
+
return { ...m, content: m.content.map((b) => b.text).join('\n') };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const diskformerLlamaBackend = {
|
|
74
|
+
id: 'diskformer-llama',
|
|
75
|
+
tasks: ['text-generation', 'image-text-to-text'],
|
|
76
|
+
source: SOURCES.diskformerLlama,
|
|
77
|
+
// Where a GGUF downloads from; a test points it at a local server.
|
|
78
|
+
url: (model, file) => `${model.endpoint || 'https://huggingface.co'}/${model.repo}/resolve/${model.revision || 'main'}/${file.name.split('/').map(encodeURIComponent).join('/')}`,
|
|
79
|
+
|
|
80
|
+
canRun(model, caps, { budget, task, file = null }) {
|
|
81
|
+
if (!model.formats.includes('gguf')) return { ok: false, code: 'format', reason: 'no GGUF in the repo' };
|
|
82
|
+
if (task && !diskformerLlamaBackend.tasks.includes(task)) return { ok: false, code: 'task', reason: `task ${task}; diskformer's llama.cpp path runs text generation only` };
|
|
83
|
+
if (!LLAMA_ARCHITECTURES.includes(model.architecture)) return { ok: false, code: 'model', reason: `architecture ${model.architecture || 'unknown'} is not one diskformer's llama.cpp path has checked (${LLAMA_ARCHITECTURES.join(', ')})` };
|
|
84
|
+
const f = pickGguf(model, file);
|
|
85
|
+
if (!f) return { ok: false, code: 'model', reason: file ? `${file} is not a single-file language-model GGUF in the repo` : `no single-file GGUF at ${QUANTS.join(', ')} in the repo` };
|
|
86
|
+
if (!caps.webgpu) return { ok: false, code: 'webgpu', reason: 'no WebGPU adapter' };
|
|
87
|
+
if (!caps.memory64 || !caps.jspi) return { ok: false, code: 'webgpu', reason: 'the llama.cpp build needs wasm memory64 and JSPI (Chrome or Edge 137+)' };
|
|
88
|
+
if (!caps.opfs) return { ok: false, code: 'opfs', reason: 'no OPFS (navigator.storage.getDirectory)' };
|
|
89
|
+
// The budget is not checked here: llama.cpp plans its expert slots from it at load time, and refuses there.
|
|
90
|
+
return { ok: true, estimate: { gpuBytes: budget, diskBytes: f.size, tokPerSec: null } };
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// maxCtx sizes the KV cache; 1024 is the context diskformer's gates run at.
|
|
94
|
+
async load(model, { budget, file = null, maxCtx = 1024, onProgress, signal }) {
|
|
95
|
+
const f = pickGguf(model, file);
|
|
96
|
+
const { Llama } = await import(diskformerLlamaBackend.source);
|
|
97
|
+
const llama = new Llama();
|
|
98
|
+
const say = (e) => onProgress?.({ backend: 'diskformer-llama', file: f.name, ...e });
|
|
99
|
+
const stop = () => llama.abort();
|
|
100
|
+
signal?.addEventListener('abort', stop, { once: true });
|
|
101
|
+
let info;
|
|
102
|
+
try {
|
|
103
|
+
const have = (await llama.list()).find((m) => m.name === f.name && !m.partial);
|
|
104
|
+
if (!have || have.size !== f.size) {
|
|
105
|
+
say({ status: 'download', loaded: 0, total: f.size });
|
|
106
|
+
await llama.download({ url: diskformerLlamaBackend.url(model, f), name: f.name, size: f.size },
|
|
107
|
+
(p) => say({ status: 'download', loaded: p.copied, total: p.total }));
|
|
108
|
+
signal?.throwIfAborted();
|
|
109
|
+
}
|
|
110
|
+
await writeIndex({ ...(await readIndex()), [f.name]: model.repo });
|
|
111
|
+
say({ status: 'load' });
|
|
112
|
+
info = await llama.load({ name: f.name, budget, ctx: maxCtx }, (p) => say({ status: 'plan', plan: p }));
|
|
113
|
+
signal?.throwIfAborted();
|
|
114
|
+
} catch (e) {
|
|
115
|
+
llama.terminate();
|
|
116
|
+
throw e;
|
|
117
|
+
} finally {
|
|
118
|
+
signal?.removeEventListener('abort', stop);
|
|
119
|
+
}
|
|
120
|
+
say({ status: 'ready', slots: info.slots });
|
|
121
|
+
return {
|
|
122
|
+
task: 'text-generation',
|
|
123
|
+
llama,
|
|
124
|
+
info,
|
|
125
|
+
// llama.cpp hands back text pieces, not token ids, so there are no think ids to report.
|
|
126
|
+
think: null,
|
|
127
|
+
// thinking: false (the default, as the other engines here) renders the template with thinking off, so a model that
|
|
128
|
+
// reasons first answers directly; true lets it think.
|
|
129
|
+
async *generate(messages, { maxNewTokens = 512, signal, thinking = false } = {}) {
|
|
130
|
+
const chunks = [];
|
|
131
|
+
let wake = null, finished = false, failed = null, result = null, text = '', tokens = 0;
|
|
132
|
+
const nudge = () => { wake?.(); wake = null; };
|
|
133
|
+
const abort = () => llama.abort();
|
|
134
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
135
|
+
const job = llama.chat(textOnly(messages), {
|
|
136
|
+
n: maxNewTokens, thinking,
|
|
137
|
+
onPiece: (s) => { text += s; tokens++; chunks.push({ text, tokens }); nudge(); },
|
|
138
|
+
}).then((r) => { result = r; }, (e) => { failed = e; }).finally(() => { finished = true; nudge(); });
|
|
139
|
+
try {
|
|
140
|
+
for (;;) {
|
|
141
|
+
while (chunks.length) yield chunks.shift();
|
|
142
|
+
if (finished) break;
|
|
143
|
+
await new Promise((r) => { wake = r; });
|
|
144
|
+
}
|
|
145
|
+
signal?.throwIfAborted();
|
|
146
|
+
if (failed) throw failed;
|
|
147
|
+
const s = result.stats || {};
|
|
148
|
+
const decode = s.decode_tokens && s.decode_ms ? s.decode_tokens / (s.decode_ms / 1000) : null;
|
|
149
|
+
yield { text: result.text, tokens: s.decode_tokens ?? tokens, ...(decode && { tokPerSec: decode }) };
|
|
150
|
+
} finally {
|
|
151
|
+
signal?.removeEventListener('abort', abort);
|
|
152
|
+
// The caller stopped reading early: stop the reply so the next call does not queue behind it.
|
|
153
|
+
if (!finished) { llama.abort(); await job; }
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
// A continuation of an already formatted prompt, greedy: what diskformer's gate replays.
|
|
157
|
+
complete: (prompt, { maxNewTokens = 64 } = {}) => llama.complete(prompt, { n: maxNewTokens }),
|
|
158
|
+
dispose: () => llama.terminate(),
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|