anyformer 0.4.17 → 0.4.19
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 +54 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/backends/diskformer-llama.js +19 -12
- package/src/load.js +8 -5
- package/src/models.js +14 -1
- package/src/sources.js +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.19 — 2026-10-31
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- diskformer pin 314362e → 317d2a0 (v0.6.18): with thinking off, diskformer's llama.cpp chat closes a chat template's
|
|
7
|
+
always-open `<think>` block and bans the `<think>` token for the reply, so LFM2.5-24B-A2B (base and
|
|
8
|
+
`LFM2_5_24B_PI`) answers directly. This resolves 0.4.18's known limitation. diskformer's gates (Chrome, 4 GB):
|
|
9
|
+
thinking off 9/9 plain replies with and without the Pi adapter; thinking on unchanged (8/9 against llama.cpp).
|
|
10
|
+
|
|
11
|
+
### Verified
|
|
12
|
+
- `LFM2_5_24B_PI` through anyformer, 4 turns, thinking off (browser `chat?entry=chat.LFM2_5_24B_PI&local=1&turns=4&tokens=96`,
|
|
13
|
+
Chrome 156, this Mac, 12:12 IST, base GGUF and adapter from local files, diskformer 317d2a0): PASS. Loaded in 101.1 s;
|
|
14
|
+
every reply on topic with no `<think>` text (CPU, RAM, which is faster with latency figures, a one-sentence
|
|
15
|
+
summary); 2.6–3.1 tok/s.
|
|
16
|
+
- LFM2.5 230M, 4 turns, on 0.4.18's pin 314362e (browser `chat?local=1&turns=4`, `localmind`, 12:13 IST): PASS, load
|
|
17
|
+
8.7 s, 4/4 turns on topic.
|
|
18
|
+
|
|
19
|
+
## 0.4.18 — 2026-10-31
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- LoRA adapters: `load(repo, { adapter: { repo, file, revision, size, scale } })` (or an entry carrying `adapter`)
|
|
23
|
+
downloads the adapter GGUF into OPFS beside the model and applies it at `scale` on `diskformer-llama`, the only back
|
|
24
|
+
end with `adapters: true`; the others drop out of the plan. Needs diskformer v0.6.17.
|
|
25
|
+
- `MODELS.chat.LFM2_5_24B_PI`: LFM2.5-24B-A2B (QAD-Q4_0) with harness-hub's Pi coding-agent LoRA
|
|
26
|
+
(`harness-hub/LFM2.5-24B-A2B-pi-GGUF` at a994cf7, 37.4 MB), 4 GB budget, `experimental`, `license: 'lfm1.0'`.
|
|
27
|
+
diskformer's gate (v0.6.17): 8/9 replies identical to llama-server --lora (7/9 without the adapter, the path's own
|
|
28
|
+
baseline). The adapter was trained inside the Pi agent; elsewhere expect base-model behaviour or worse. Hub fixture
|
|
29
|
+
at 0912d33.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- diskformer pin f17009c → 314362e (v0.6.17, which includes v0.6.16). v0.6.16: Chrome 156 rejects mixed f16/f32 subgroup-matrix kernels on Apple GPUs,
|
|
33
|
+
so Ternary Bonsai 2 27B, Gemma 4 E2B, LFM2.5 and the IQ-quant dense path failed to load ("Mixed precision subgroup
|
|
34
|
+
matrix multiply is not supported by the device"). diskformer's new `engines/subgroup-matrix-guard.js` hides the
|
|
35
|
+
feature when a tiny mixed kernel fails to compile, and those engines take their non-matrix prefill (slower prefill).
|
|
36
|
+
- Browser tests: `chat` takes `?turns=N` (up to 4 follow-up turns, each checked on topic); `vision` takes `?backend=`
|
|
37
|
+
and `?local=1` (MiniCPM-V 4.7 on diskformer, image as packed RGB after `ensureVision()`); `run.mjs` prints each
|
|
38
|
+
failed network request with its reason. `vision?local=1` serves `cats.jpg` from `--models` as well. `chat` also takes
|
|
39
|
+
`?entry=<family>.<KEY>` (a MODELS entry, adapter included) and `?tokens=N`; its `?local=1` serves diskformer-llama
|
|
40
|
+
GGUFs, adapters and (through the engines' fetch) localmind GGUFs by file name.
|
|
41
|
+
|
|
42
|
+
### Known limitation
|
|
43
|
+
- LFM2.5-24B-A2B's chat template always opens a `<think>` block and ignores `enable_thinking`, so on
|
|
44
|
+
`diskformer-llama` a reply with thinking off still starts with visible reasoning, then `</think>`, then the answer.
|
|
45
|
+
The fix belongs in diskformer's chat driver (close the block when thinking is off).
|
|
46
|
+
|
|
47
|
+
### Verified
|
|
48
|
+
- `LFM2_5_24B_PI` through anyformer (browser `chat?entry=chat.LFM2_5_24B_PI&local=1&tokens=400`, Chrome 156, this
|
|
49
|
+
Mac, 11:56 IST, base GGUF and adapter from local files, adapter sha256 5e96d059…, diskformer 314362e): loaded with
|
|
50
|
+
the adapter in 208.9 s; reasoning, `</think>`, then a correct two-sentence answer about the CPU; 162 tokens at
|
|
51
|
+
2.73 tok/s. The page reports FAIL because its check rejects `</think>` text (the limitation above). A 4-turn run at
|
|
52
|
+
64 tokens per reply spent each reply on reasoning (1.5–3.0 tok/s).
|
|
53
|
+
- MiniCPM-V 4.7 image round trip through anyformer (browser `vision`, Chrome 156, this Mac, 11:12 IST, GGUF, vision
|
|
54
|
+
encoder and image from local files, 6.5 GB budget, diskformer f17009c): "There are **two cats** in this picture",
|
|
55
|
+
then a correct description, 40 tokens at 18.3 tok/s.
|
|
56
|
+
|
|
3
57
|
## 0.4.17 — 2026-10-30
|
|
4
58
|
|
|
5
59
|
### Added
|
package/README.md
CHANGED
|
@@ -76,8 +76,8 @@ and `threw`. Every other failure is an `AnyformerError` too, with a code from `E
|
|
|
76
76
|
## Commands
|
|
77
77
|
|
|
78
78
|
```js
|
|
79
|
-
plan(repo, { budget, task }) // what would run it, and why the others would not; loads nothing
|
|
80
|
-
load(repo, { budget, task, file, maxCtx, poolBytes, localFile, fetch, ple, speculative, onProgress, signal }) // → { backend, task, plan, run(input), generate?, think?, prime?, reset?, speculative?, dispose() }
|
|
79
|
+
plan(repo, { budget, task }) // what would run it (adapter: a LoRA GGUF; only diskformer-llama applies one), and why the others would not; loads nothing
|
|
80
|
+
load(repo, { budget, task, file, adapter, maxCtx, poolBytes, localFile, fetch, ple, speculative, onProgress, signal }) // → { backend, task, plan, run(input), generate?, think?, prime?, reset?, speculative?, dispose() }
|
|
81
81
|
pipeline(task, repo, { budget }) // transformers.js's call shape: gen(messages) → [{ generated_text }]
|
|
82
82
|
probe() // WebGPU, shader-f16, subgroups, limits, OPFS quota, memory64
|
|
83
83
|
describe(repo) // Hub repo → formats, architecture, task, tags, files
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyformer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
4
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
6
|
"main": "./index.js",
|
|
@@ -112,6 +112,8 @@ function textOnly(messages) {
|
|
|
112
112
|
export const diskformerLlamaBackend = {
|
|
113
113
|
id: 'diskformer-llama',
|
|
114
114
|
tasks: ['text-generation', 'image-text-to-text'],
|
|
115
|
+
// Applies a LoRA GGUF over the model (llama.load({ lora, loraScale }), diskformer v0.6.17+), as llama-server --lora.
|
|
116
|
+
adapters: true,
|
|
115
117
|
source: SOURCES.diskformerLlama,
|
|
116
118
|
// Where a GGUF downloads from; a test points it at a local server.
|
|
117
119
|
url: (model, file) => `${model.endpoint || 'https://huggingface.co'}/${model.repo}/resolve/${model.revision || 'main'}/${file.name.split('/').map(encodeURIComponent).join('/')}`,
|
|
@@ -131,7 +133,8 @@ export const diskformerLlamaBackend = {
|
|
|
131
133
|
},
|
|
132
134
|
|
|
133
135
|
// maxCtx sizes the KV cache; 1024 is the context diskformer's gates run at.
|
|
134
|
-
|
|
136
|
+
// adapter: { repo, file, revision, size, scale } downloads into OPFS beside the model and applies at scale (default 1).
|
|
137
|
+
async load(model, { budget, file = null, adapter = null, maxCtx = 1024, onProgress, signal }) {
|
|
135
138
|
const f = pickGguf(model, file);
|
|
136
139
|
const { Llama } = await import(diskformerLlamaBackend.source);
|
|
137
140
|
const llama = new Llama();
|
|
@@ -140,23 +143,27 @@ export const diskformerLlamaBackend = {
|
|
|
140
143
|
signal?.addEventListener('abort', stop, { once: true });
|
|
141
144
|
let info;
|
|
142
145
|
try {
|
|
143
|
-
// Each part
|
|
144
|
-
const
|
|
146
|
+
// Each part, side file and adapter downloads under its own name; progress counts bytes over all of them.
|
|
147
|
+
const lora = adapter && { name: adapter.file.split('/').pop(), size: adapter.size, repo: adapter.repo,
|
|
148
|
+
url: diskformerLlamaBackend.url({ repo: adapter.repo, revision: adapter.revision, endpoint: model.endpoint }, { name: adapter.file }) };
|
|
149
|
+
const files = [...f.parts.map((p) => ({ name: opfsName(p), size: p.size, repo: model.repo, url: diskformerLlamaBackend.url(model, p) })),
|
|
150
|
+
...f.extra.map((p) => ({ name: opfsName(p), size: p.size, repo: model.repo, url: diskformerLlamaBackend.url(model, p) })),
|
|
151
|
+
...(lora ? [lora] : [])];
|
|
152
|
+
const total = files.reduce((n, x) => n + x.size, 0);
|
|
145
153
|
let before = 0;
|
|
146
|
-
for (const
|
|
147
|
-
const name = opfsName(part);
|
|
154
|
+
for (const { name, size, url } of files) {
|
|
148
155
|
const have = (await llama.list()).find((m) => m.name === name && !m.partial);
|
|
149
|
-
if (!have || have.size !==
|
|
150
|
-
say({ status: 'download', loaded: before, total
|
|
151
|
-
await llama.download({ url
|
|
152
|
-
(p) => say({ status: 'download', loaded: before + p.copied, total: f.size }));
|
|
156
|
+
if (!have || have.size !== size) {
|
|
157
|
+
say({ status: 'download', loaded: before, total });
|
|
158
|
+
await llama.download({ url, name, size }, (p) => say({ status: 'download', loaded: before + p.copied, total }));
|
|
153
159
|
signal?.throwIfAborted();
|
|
154
160
|
}
|
|
155
|
-
before +=
|
|
161
|
+
before += size;
|
|
156
162
|
}
|
|
157
|
-
await writeIndex({ ...(await readIndex()), ...Object.fromEntries(files.map((p) => [
|
|
163
|
+
await writeIndex({ ...(await readIndex()), ...Object.fromEntries(files.map((p) => [p.name, p.repo])) });
|
|
158
164
|
say({ status: 'load' });
|
|
159
|
-
info = await llama.load({ name: opfsName(f), budget, ctx: maxCtx, extra: f.extra.map(opfsName)
|
|
165
|
+
info = await llama.load({ name: opfsName(f), budget, ctx: maxCtx, extra: f.extra.map(opfsName),
|
|
166
|
+
...(lora && { lora: lora.name, loraScale: adapter.scale ?? 1 }) }, (p) => say({ status: 'plan', plan: p }));
|
|
160
167
|
signal?.throwIfAborted();
|
|
161
168
|
} catch (e) {
|
|
162
169
|
llama.terminate();
|
package/src/load.js
CHANGED
|
@@ -29,16 +29,19 @@ function noBackend(plan) {
|
|
|
29
29
|
// What load() would do, without loading: the descriptor, the caps, and every back end's answer.
|
|
30
30
|
// task: a Hugging Face pipeline task ('text-generation', 'feature-extraction', …); null runs the repo's own.
|
|
31
31
|
// file: a weights file in the repo to load (a GGUF name); null lets each back end pick.
|
|
32
|
-
// repo: a Hub id, or an entry from MODELS (its task and
|
|
32
|
+
// repo: a Hub id, or an entry from MODELS (its task, budget and adapter apply unless opts set them).
|
|
33
|
+
// adapter: a LoRA GGUF applied over the model, { repo, file, revision, size, scale }; only a back end with
|
|
34
|
+
// `adapters: true` (diskformer-llama) can apply one, so the others drop out of the plan.
|
|
33
35
|
const entry = (repo, opts) => (typeof repo === 'object' && repo?.repo
|
|
34
|
-
? [repo.repo, { ...(repo.task && { task: repo.task }), ...(repo.budget && { budget: repo.budget }), ...opts }]
|
|
36
|
+
? [repo.repo, { ...(repo.task && { task: repo.task }), ...(repo.budget && { budget: repo.budget }), ...(repo.adapter && { adapter: repo.adapter }), ...opts }]
|
|
35
37
|
: [repo, opts]);
|
|
36
38
|
|
|
37
39
|
export async function plan(repoOrEntry, opts = {}) {
|
|
38
|
-
let [repo, { budget = DEFAULT_BUDGET, task = null, file = null, backends = BACKENDS, caps, model, fetch }] = entry(repoOrEntry, opts);
|
|
40
|
+
let [repo, { budget = DEFAULT_BUDGET, task = null, file = null, backends = BACKENDS, caps, model, fetch, adapter = null }] = entry(repoOrEntry, opts);
|
|
39
41
|
caps ??= await probe();
|
|
40
42
|
model ??= await describe(repo, fetch ? { fetch } : {});
|
|
41
|
-
|
|
43
|
+
if (adapter) backends = backends.filter((b) => b.adapters);
|
|
44
|
+
return { model, caps, budget, task, file, ...(adapter && { adapter }), ...(await resolve(model, caps, backends, { budget, task, file })) };
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
export async function load(repoOrEntry, options = {}) {
|
|
@@ -50,7 +53,7 @@ export async function load(repoOrEntry, options = {}) {
|
|
|
50
53
|
const backend = backends.find((b) => b.id === p.chosen);
|
|
51
54
|
let session;
|
|
52
55
|
try {
|
|
53
|
-
session = await backend.load(p.model, { budget: p.budget, task: p.task, file: p.file, caps: p.caps, maxCtx, dtype, poolBytes, localFile, fetch: engineFetch, ple, speculative, speech, onProgress, signal });
|
|
56
|
+
session = await backend.load(p.model, { budget: p.budget, task: p.task, file: p.file, adapter: p.adapter, caps: p.caps, maxCtx, dtype, poolBytes, localFile, fetch: engineFetch, ple, speculative, speech, onProgress, signal });
|
|
54
57
|
} catch (e) { throw wrap(e, 'LOAD_FAILED', signal, `${backend.id}: `); }
|
|
55
58
|
// Added onto serialize()'s object, not spread: a spread would read its getters once and freeze them.
|
|
56
59
|
return Object.assign(serialize(session), { backend: backend.id, task: session.task || p.task || p.model.task || null, ...(session.device && { device: session.device }), plan: p });
|
package/src/models.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// passed. `test` names the page under test/browser/ that proves the entry; test/models.test.js checks it exists.
|
|
4
4
|
// `diffusion: true` marks a text-diffusion model (the reply denoises in blocks, so generate() chunks arrive a block at a time); `experimental: true` marks an entry its
|
|
5
5
|
// app shows as opt-in until that app's own checks pass; `audioOut: true` marks a model that can also answer in speech
|
|
6
|
-
// (generate({ speech: true })).
|
|
6
|
+
// (generate({ speech: true })); `adapter` names a LoRA GGUF applied over the model (diskformer-llama only).
|
|
7
7
|
|
|
8
8
|
const M = (repo, backend, task, test, extra = {}) => ({ repo, backend, task, test, ...extra });
|
|
9
9
|
|
|
@@ -32,6 +32,17 @@ const NEMOTRON_3_5_BOLT_E4B = M('danyalmalik/NVIDIA-Nemotron-3.5-Bolt-E4B-ONNX-W
|
|
|
32
32
|
// diskformer checked one hand-written prompt through complete(); generate() renders the GGUF's chat template, which no
|
|
33
33
|
// browser run has checked.
|
|
34
34
|
const DEEPSEEK_V4_1_FLASH = M('ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF', 'diskformer-llama', 'text-generation', 'llama-gate', { budget: 8e9, downloadBytes: 231316411072, experimental: true });
|
|
35
|
+
// LFM2.5-24B-A2B with harness-hub's Pi LoRA (rank 32, alpha 64; asynchronous GRPO inside the Pi coding agent), on
|
|
36
|
+
// diskformer's llama.cpp path, the only back end that applies an adapter. The adapter touches the conv, attention and
|
|
37
|
+
// two dense FFN layers only, so expert paging is unchanged. diskformer v0.6.17's gate: 8/9 replies identical to
|
|
38
|
+
// llama-server --lora (the path's own baseline without the adapter is 7/9). Authors' numbers on 100 data-analysis tasks:
|
|
39
|
+
// 39% fewer tool calls on tasks both solve, pass@1 −4.2 points (noise about ±5); expect base-model behaviour or worse in
|
|
40
|
+
// other agents. Licence lfm1.0 (model and adapter): royalty-free commercial use only below US$10M annual revenue.
|
|
41
|
+
const LFM2_5_24B_PI = M('LiquidAI/LFM2.5-24B-A2B-GGUF', 'diskformer-llama', 'text-generation', 'chat', {
|
|
42
|
+
budget: 4e9, downloadBytes: 13578190880 + 37437888, experimental: true, license: 'lfm1.0',
|
|
43
|
+
adapter: { repo: 'harness-hub/LFM2.5-24B-A2B-pi-GGUF', file: 'LFM2.5-24B-A2B-harbor-pi-lora-F16.gguf',
|
|
44
|
+
revision: 'a994cf7f03ab0e572832d5829f42cbe2ef28d472', size: 37437888, scale: 1, trainedFor: 'pi' },
|
|
45
|
+
});
|
|
35
46
|
const MINILM_L6 = M('Xenova/all-MiniLM-L6-v2', 'transformers', 'feature-extraction', 'embed', { downloadBytes: 90e6 });
|
|
36
47
|
const DETR_RESNET_50 = M('Xenova/detr-resnet-50', 'transformers', 'object-detection', 'detect', { downloadBytes: 167e6 });
|
|
37
48
|
|
|
@@ -69,6 +80,8 @@ export const MODELS = {
|
|
|
69
80
|
LFM3_OMNI_2B,
|
|
70
81
|
/** DeepSeek V4.1 Flash (763B MoE), a tech demo on diskformer's llama.cpp path: 231 GB on disk, very slow. */
|
|
71
82
|
DEEPSEEK_V4_1_FLASH,
|
|
83
|
+
/** LFM2.5-24B-A2B with harness-hub's Pi coding-agent LoRA (diskformer's llama.cpp path); tuned for the Pi agent; licence lfm1.0. */
|
|
84
|
+
LFM2_5_24B_PI,
|
|
72
85
|
},
|
|
73
86
|
/** Chat with images in the messages: content [{ type: 'image', image }, { type: 'text', text }]. Gemma 4 takes audio too. */
|
|
74
87
|
vision: { DEFAULT: QWEN3_5_4B, QWEN3_5_4B, GEMMA4_E2B_ONNX, GEMMA4_E4B_ONNX },
|
package/src/sources.js
CHANGED
|
@@ -9,6 +9,13 @@ export const PINS = {
|
|
|
9
9
|
// v0.3.1: seeded block-KV graphs (Nemotron-Labs-Diffusion-3B), chatFormat, split external data; download progress
|
|
10
10
|
// (progress_callback) and the graph files cached in Cache Storage 'kohra-models'.
|
|
11
11
|
kohra: 'bf95d44c3e3cfc001201baa03fa121c2bf9c7d73',
|
|
12
|
+
// v0.6.18 (317d2a0): with thinking off, the llama.cpp chat closes a template's always-open <think> (LFM2.5-24B) and
|
|
13
|
+
// bans the <think> token for the reply.
|
|
14
|
+
// v0.6.17 (314362e): LoRA adapters on the llama.cpp path (llama.load({ lora, loraScale })), and lfm2_moe_ssd.js runs
|
|
15
|
+
// a LoRA-merged LFM2.5-24B-A2B with Q8_0 dense matrices.
|
|
16
|
+
// v0.6.16 (467052f): engines/subgroup-matrix-guard.js. Chrome 156 rejects mixed f16/f32 subgroup-matrix kernels
|
|
17
|
+
// on Apple GPUs; the guard hides the feature when one tiny mixed kernel fails to compile, so Ternary Bonsai 2 27B,
|
|
18
|
+
// Gemma 4 E2B, LFM2.5 and qwen3_moe_ssd.js take their non-matrix prefill (slower prefill, same output path).
|
|
12
19
|
// v0.6.15 (f17009c): engines/clover_moe_ssd.js, Clover-35B-A3B-Preview (block diffusion on Qwen3.6-35B-A3B; kohra's
|
|
13
20
|
// denoise loop vendored in engines/vendor/). qwen35_moe_ssd.js and qwen3_moe_ssd.js only learn to accept clover_moe.
|
|
14
21
|
// v0.6.14 (93193d0): LFM3-Omni speech out's inverse STFT uses an FFT (same samples), 0.54x → 1.5–1.8x real time on
|
|
@@ -23,7 +30,7 @@ export const PINS = {
|
|
|
23
30
|
// Gemma 4 E2B/E4B and Ternary Bonsai 2) under engines/, and the llama.cpp path under ggml/, which loads split GGUFs
|
|
24
31
|
// and drafters, and reads past 2 GiB (Nemotron 3.5 Bolt E4B needs v0.6.8 or later). The hand engines' ingest
|
|
25
32
|
// streams the GGUF header with progress and resumes a dropped range.
|
|
26
|
-
diskformer: '
|
|
33
|
+
diskformer: '317d2a002d95278e504ed84fab2d0f71dabaa884',
|
|
27
34
|
};
|
|
28
35
|
|
|
29
36
|
const gh = (repo, sha, path) => `https://cdn.jsdelivr.net/gh/NakliTechie/${repo}@${sha}/${path}`;
|