anyformer 0.4.20 → 0.4.22
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 +44 -0
- package/package.json +1 -1
- package/src/backends/diskformer.js +43 -3
- package/src/sources.js +3 -1
- package/src/store.js +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.22 — 2026-10-31
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- The `diskformer` back end downloads an `adapter` GGUF itself and keeps it in Cache Storage (`anyformer-adapters-v1`,
|
|
7
|
+
keyed by its URL, so by repo, revision and file). A later load reads it from disk; a cached copy whose size
|
|
8
|
+
differs from `adapter.size` downloads again. The download reports progress (`status: 'adapter'`, `loaded`,
|
|
9
|
+
`total`; `cached: true` on a cache hit), and the engine gets the bytes as a Blob. Before, every load fetched the
|
|
10
|
+
37 MB Pi adapter again with no progress (NakliOS: about 3 minutes per load at 200 KB/s).
|
|
11
|
+
- `stored()` and `remove(repo)` cover the adapter cache, listed under the adapter's repo.
|
|
12
|
+
|
|
13
|
+
### Verified
|
|
14
|
+
- `npm test` 108/108: the first load downloads the adapter once with progress from 0 to 37,437,888 bytes and caches
|
|
15
|
+
it; a second load downloads nothing and reports `cached: true`.
|
|
16
|
+
|
|
17
|
+
### Not yet verified
|
|
18
|
+
- No browser run of the cache in Chrome; NakliOS's live check covers it.
|
|
19
|
+
|
|
20
|
+
## 0.4.21 — 2026-10-31
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- diskformer pin cefebc7 → 7962912 (v0.6.20: run-time LoRA in every native MoE engine through `engines/lora.js`).
|
|
24
|
+
The `diskformer` back end now takes `adapter` on Gemma 4 26B and Qwen3.6 35B too. diskformer's gates (plain /
|
|
25
|
+
adapter / plain again, against llama-server --lora on the same file): Gemma 4 26B 9/9, 8/9, 9/9; Qwen3.6 35B 6/9,
|
|
26
|
+
9/9, 6/9. Adapters on routed experts are refused with an error. MiniCPM-V, Clover, Flint and Ling take no adapter,
|
|
27
|
+
so an `adapter` drops `diskformer` from their plans.
|
|
28
|
+
|
|
29
|
+
### Verified (after the tag, on the pinned code)
|
|
30
|
+
- Browser test `diskformer-chat --query "local=1&adapter=gemma4-26b-random-lora.gguf"` (new adapter mode; Gemma 4 26B
|
|
31
|
+
at a 2.5 GB budget, diskformer's seeded random rank-32 test adapter on 205 matrices, thinking on, 15:28 to 15:35
|
|
32
|
+
IST): plain 9/9 against llama-server, adapter on 8/9 against `llama-server --lora` on the same file, plain again 9/9
|
|
33
|
+
with the same text as the first phase; the adapter changed 3 of 9 replies. The one miss (word-problem) is the one
|
|
34
|
+
diskformer's own gate reports: the reply where the adapter first changes the text keeps the plain branch.
|
|
35
|
+
- Not run: Qwen3.6 35B with its test adapter (36.9 GB GGUF).
|
|
36
|
+
|
|
3
37
|
## 0.4.20 — 2026-10-31
|
|
4
38
|
|
|
5
39
|
### Added
|
|
@@ -24,6 +58,16 @@
|
|
|
24
58
|
`alwaysThinks` and `thinkInPrompt`) returns only the answer in `text`, and the reasoning in `reasoning`.
|
|
25
59
|
- diskformer pin 317d2a0 → cefebc7 (v0.6.19: run-time LoRA in `lfm2_moe_ssd.js`).
|
|
26
60
|
|
|
61
|
+
### Verified (after the npm publish, on the published code)
|
|
62
|
+
- `tools` on LFM2.5 230M (`localmind`, local file): `get_weather` {"city":"Paris"} parsed from the bare call list;
|
|
63
|
+
after the tool result, an answer with 18°C and clear sky.
|
|
64
|
+
- `tools` on `LFM2_5_24B_PI` (`diskformer`, run-time adapter, base GGUF and adapter from local files, 13:31 IST): the
|
|
65
|
+
first turn's text is empty (reasoning in `reasoning`) with `get_weather` {"city":"Paris"} as `toolCalls`; then
|
|
66
|
+
"The current weather in Paris is **clear skies** with a temperature of **18°C**."
|
|
67
|
+
- `chat?entry=chat.LFM2_5_24B_PI&turns=4` on the same engine (by 13:41 IST): PASS, 4/4 turns on topic with no
|
|
68
|
+
reasoning in `text`; load 50 s from OPFS. The page's tok/s (about 2.5) counts only answer tokens after the hidden
|
|
69
|
+
reasoning, so it is not a decode speed.
|
|
70
|
+
|
|
27
71
|
## 0.4.19 — 2026-10-31
|
|
28
72
|
|
|
29
73
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anyformer",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.22",
|
|
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",
|
|
@@ -11,11 +11,15 @@ export const DISKFORMER_MODELS = [
|
|
|
11
11
|
{
|
|
12
12
|
repo: 'google/gemma-4-26B-A4B-it-qat-q4_0-gguf', file: 'gemma-4-26B_q4_0-it.gguf', size: 14439363584,
|
|
13
13
|
engine: 'gemma4_moe_ssd.js', Engine: 'Gemma4MoeSsd', minBudget: 2.5e9,
|
|
14
|
+
// LoRA at run time (v0.6.20+, gated against llama-server --lora on the same adapter file).
|
|
15
|
+
adapters: true,
|
|
14
16
|
speeds: [[2.5e9, 12.4], [4.3e9, 14.2], [7e9, 17.9]],
|
|
15
17
|
},
|
|
16
18
|
{
|
|
17
19
|
repo: 'unsloth/Qwen3.6-35B-A3B-GGUF', file: 'Qwen3.6-35B-A3B-Q8_0.gguf', size: 36903140320,
|
|
18
20
|
engine: 'qwen35_moe_ssd.js', Engine: 'Qwen35MoeSsd', minBudget: 4e9,
|
|
21
|
+
// LoRA at run time (v0.6.20+, gated against llama-server --lora on the same adapter file).
|
|
22
|
+
adapters: true,
|
|
19
23
|
speeds: [[4e9, 9.2]],
|
|
20
24
|
},
|
|
21
25
|
// Qwen3.6's language model with a vision encoder (mmproj, ~1.1 GB, loaded on the first image by ensureVision).
|
|
@@ -77,6 +81,41 @@ function budgetError(e) {
|
|
|
77
81
|
return m && new AnyformerError('BUDGET_TOO_SMALL', `diskformer: ${e.message}`, { cause: e, need: Math.ceil((+m[1] + +m[2]) * 1e9) });
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
// Adapter GGUFs, kept in Cache Storage under their download URL (…/<repo>/resolve/<revision>/<file>), so a later load
|
|
85
|
+
// reads them from disk. src/store.js lists and removes them with the adapter's repo.
|
|
86
|
+
export const ADAPTER_CACHE = 'anyformer-adapters-v1';
|
|
87
|
+
|
|
88
|
+
// The adapter's bytes as a Blob: from the cache, or downloaded with progress events and then cached. A cached copy
|
|
89
|
+
// whose size differs from adapter.size is downloaded again.
|
|
90
|
+
async function adapterBlob(url, adapter, onProgress, signal) {
|
|
91
|
+
const cache = typeof caches !== 'undefined' ? await caches.open(ADAPTER_CACHE).catch(() => null) : null;
|
|
92
|
+
const say = (loaded, total, cached = false) => onProgress?.({ backend: 'diskformer', status: 'adapter', file: adapter.file, loaded, total, ...(cached && { cached }) });
|
|
93
|
+
const hit = await cache?.match(url);
|
|
94
|
+
if (hit) {
|
|
95
|
+
const b = await hit.blob();
|
|
96
|
+
if (!adapter.size || b.size === adapter.size) { say(b.size, b.size, true); return b; }
|
|
97
|
+
await cache.delete(url);
|
|
98
|
+
}
|
|
99
|
+
const res = await fetch(url, { signal });
|
|
100
|
+
if (!res.ok) throw new AnyformerError('LOAD_FAILED', `diskformer: adapter ${adapter.file}: HTTP ${res.status}`);
|
|
101
|
+
const total = Number(res.headers.get('content-length')) || adapter.size || 0;
|
|
102
|
+
const parts = [], reader = res.body.getReader();
|
|
103
|
+
let loaded = 0, at = 0;
|
|
104
|
+
say(0, total);
|
|
105
|
+
for (;;) {
|
|
106
|
+
const { done, value } = await reader.read();
|
|
107
|
+
if (done) break;
|
|
108
|
+
parts.push(value);
|
|
109
|
+
loaded += value.length;
|
|
110
|
+
if (Date.now() - at > 250) { at = Date.now(); say(loaded, total); }
|
|
111
|
+
}
|
|
112
|
+
const blob = new Blob(parts);
|
|
113
|
+
if (adapter.size && blob.size !== adapter.size) throw new AnyformerError('LOAD_FAILED', `diskformer: adapter ${adapter.file}: ${blob.size} bytes, expected ${adapter.size}`);
|
|
114
|
+
say(blob.size, total || blob.size);
|
|
115
|
+
await cache?.put(url, new Response(blob, { headers: { 'content-length': String(blob.size) } })).catch(() => {});
|
|
116
|
+
return blob;
|
|
117
|
+
}
|
|
118
|
+
|
|
80
119
|
const has = (model, file, size) => model.files.gguf.some((f) => f.name === file && f.size === size);
|
|
81
120
|
const match = (model) => DISKFORMER_MODELS.find((m) => m.repo === model.repo &&
|
|
82
121
|
(m.parts ? m.parts.every((p) => has(model, p.file, p.size)) : has(model, m.file, m.size)));
|
|
@@ -117,7 +156,8 @@ export const diskformerBackend = {
|
|
|
117
156
|
// its pool from gpuBudgetBytes when given one, so it gets one or the other).
|
|
118
157
|
// localFile: the GGUF as a File the user picked; the engine ingests it instead of downloading, and refuses a file
|
|
119
158
|
// whose size differs from the one it is pinned to.
|
|
120
|
-
// adapter: { repo, file, revision, scale } loads onto the GPU beside the model and is on for every reply;
|
|
159
|
+
// adapter: { repo, file, revision, size, scale } loads onto the GPU beside the model and is on for every reply; its
|
|
160
|
+
// GGUF downloads once (progress events with status 'adapter') and stays in Cache Storage (ADAPTER_CACHE);
|
|
121
161
|
// session.setAdapter(false / true) switches it off and on again with no reload.
|
|
122
162
|
async load(model, { budget, maxCtx = 1024, poolBytes, localFile, adapter = null, onProgress, signal }) {
|
|
123
163
|
const m = match(model);
|
|
@@ -135,8 +175,8 @@ export const diskformerBackend = {
|
|
|
135
175
|
let lora = null;
|
|
136
176
|
if (adapter) {
|
|
137
177
|
try {
|
|
138
|
-
|
|
139
|
-
lora = await engine.loadAdapter(
|
|
178
|
+
const bytes = await adapterBlob(diskformerBackend.adapterUrl(adapter), adapter, onProgress, signal);
|
|
179
|
+
lora = await engine.loadAdapter(bytes, { scale: adapter.scale ?? 1 });
|
|
140
180
|
await engine.setAdapter(lora);
|
|
141
181
|
} catch (e) { await engine.dispose?.(); throw e; }
|
|
142
182
|
}
|
package/src/sources.js
CHANGED
|
@@ -9,6 +9,8 @@ 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.20 (7962912): run-time LoRA in every native MoE engine (engines/lora.js): Gemma 4 26B, Qwen3.6 35B, LFM2.5
|
|
13
|
+
// 24B and Qwen3 (untested); adapters on routed experts are refused. Engines expose addBos from the GGUF.
|
|
12
14
|
// v0.6.19 (cefebc7): run-time LoRA in lfm2_moe_ssd.js (engine.loadAdapter / setAdapter, no reload to switch).
|
|
13
15
|
// v0.6.18 (317d2a0): with thinking off, the llama.cpp chat closes a template's always-open <think> (LFM2.5-24B) and
|
|
14
16
|
// bans the <think> token for the reply.
|
|
@@ -31,7 +33,7 @@ export const PINS = {
|
|
|
31
33
|
// Gemma 4 E2B/E4B and Ternary Bonsai 2) under engines/, and the llama.cpp path under ggml/, which loads split GGUFs
|
|
32
34
|
// and drafters, and reads past 2 GiB (Nemotron 3.5 Bolt E4B needs v0.6.8 or later). The hand engines' ingest
|
|
33
35
|
// streams the GGUF header with progress and resumes a dropped range.
|
|
34
|
-
diskformer: '
|
|
36
|
+
diskformer: '79629125d7a39b13ddcaf74df58262458d8d470e',
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
const gh = (repo, sha, path) => `https://cdn.jsdelivr.net/gh/NakliTechie/${repo}@${sha}/${path}`;
|
package/src/store.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// Cache Storage 'transformers-cache' (transformers.js; kohra's tokenizer too); 'gguf-v1', 'gguf-v1-headers' and
|
|
5
5
|
// 'bonsai-pipeline-v1' (LocalMind's engines; 'gguf-v1' holds the LFM2
|
|
6
6
|
// engine), 'kohra-models' (kohra v0.3.1+: its ONNX graph and data files), 'diskformer-models' (diskformer's
|
|
7
|
-
// ONNX engines, LFM3-Omni)
|
|
7
|
+
// ONNX engines, LFM3-Omni), 'anyformer-adapters-v1' (LoRA adapters for diskformer's engines, listed
|
|
8
|
+
// under the adapter's repo): one entry per file URL
|
|
8
9
|
// (…/<repo>/resolve/<rev>/<file>). Older kohra left its graph in the HTTP cache, which a page cannot list.
|
|
9
10
|
// IndexedDB 'gguf-cache-v1' (Bonsai 2) and 'safetensors-cache-v1' (Gemma 4 QAT-mobile): a store keyed by URL
|
|
10
11
|
// with { size } and a chunk store keyed [url, start, end]; 'webgpu-prefix-snapshots' (Bonsai 2's
|
|
@@ -15,11 +16,11 @@
|
|
|
15
16
|
// 'gguf/<name>' (diskformer-llama: llama.cpp's GGUFs, with <name>.part and .part.json while downloading;
|
|
16
17
|
// anyformer-llama.json maps each name to its repo).
|
|
17
18
|
|
|
18
|
-
import { DISKFORMER_MODELS } from './backends/diskformer.js';
|
|
19
|
+
import { DISKFORMER_MODELS, ADAPTER_CACHE } from './backends/diskformer.js';
|
|
19
20
|
import { SPLIT_ROOT } from './gguf-split.js';
|
|
20
21
|
import { readLlamaIndex, LLAMA_INDEX } from './backends/diskformer-llama.js';
|
|
21
22
|
|
|
22
|
-
const CACHE_BACKEND = { 'transformers-cache': 'transformers', 'gguf-v1': 'localmind', 'gguf-v1-headers': 'localmind', 'bonsai-pipeline-v1': 'localmind', 'kohra-models': 'kohra', 'diskformer-models': 'diskformer-onnx' };
|
|
23
|
+
const CACHE_BACKEND = { [ADAPTER_CACHE]: 'diskformer', 'transformers-cache': 'transformers', 'gguf-v1': 'localmind', 'gguf-v1-headers': 'localmind', 'bonsai-pipeline-v1': 'localmind', 'kohra-models': 'kohra', 'diskformer-models': 'diskformer-onnx' };
|
|
23
24
|
const IDB_STORES = ['gguf-cache-v1', 'safetensors-cache-v1'];
|
|
24
25
|
const BONSAI2 = 'prism-ml/Ternary-Bonsai-2-27B-gguf';
|
|
25
26
|
|