anyformer 0.4.2 → 0.4.4

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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.4 — 2026-10-24
4
+
5
+ ### Changed
6
+ - diskformer pinned to v0.6.7 (663b2b0). Its hand engines' ingest (src/ingest.js) now streams the GGUF header with
7
+ progress, so a slow link no longer goes silent for minutes (Flint at 0.1 MB/s stalled LocalMind's watchdog for 3 min).
8
+ A range that drops mid-stream, or returns 5xx or 429, resumes from the last byte, up to 6 times. The new events reach
9
+ `onProgress` as status `weights` with phase `header` or `retry`. diskformer changed only src/ingest.js and its test
10
+ between v0.6.6 and v0.6.7.
11
+
12
+ ### Verified
13
+ - npm test 87/87. Not run here in a browser: LocalMind's throttled production load of Flint is that check.
14
+
15
+ ## 0.4.3 — 2026-10-24
16
+
17
+ npm skips 0.4.2: this release carries it (Flint 40B and LFM2.5 24B on the hand engines).
18
+
19
+ ### Added
20
+ - `diskformer-llama` loads split GGUFs (`name-00001-of-0000n.gguf`): `pickGguf` returns a set named by its first part,
21
+ with every part and their summed size; a set missing a part is refused. Each part downloads into OPFS under its own
22
+ file name and `load` names the first, as diskformer v0.6.5's worker expects. Ling 3.0 Flash (77 GB, 2 parts) now
23
+ routes to the llama.cpp path under the hand engine's 6.8 GB floor (diskformer v0.6.5: 6 GB, 4.1–4.9 tok/s, 6/9 vs
24
+ llama.cpp on the CPU) and stays on the hand engine above it. Ling 3.0 Flash through anyformer in a browser is not yet run.
25
+
26
+ ### Changed
27
+ - diskformer pinned to v0.6.6 (e07e129), whose llama.cpp worker loads split GGUFs.
28
+ - OPFS names for llama.cpp's GGUFs drop the repo's folders (gguf/<file name>); a file in a repo subfolder failed before.
29
+
30
+ ### Verified
31
+ - Granite 4.0 H Tiny through `diskformer-llama` on v0.6.6, Chrome on the M4 Pro: 9/9 replies identical to llama.cpp, chat path equal to the reference.
32
+ - LocalMind's checks: through 0.4.1, LFM2.5 230M, Gemma 4 E2B and Ternary Bonsai 2 27B gave identical replies on 21/21 rows in both run orders, with decode within run-to-run spread; through 0.4.2, Flint 40B and LFM2.5 24B gave identical replies 3/3 in four runs, with equal GPU memory and equal or faster decode.
33
+
3
34
  ## 0.4.2 — 2026-10-23
4
35
 
5
36
  ### Added
package/README.md CHANGED
@@ -14,7 +14,7 @@ browser storage; no account, no server, no telemetry.
14
14
  | How | Command |
15
15
  |---|---|
16
16
  | npm | `npm install anyformer` |
17
- | CDN | `import { load } from 'https://cdn.jsdelivr.net/npm/anyformer@0.1.0/+esm';` |
17
+ | CDN | `import { load } from 'https://cdn.jsdelivr.net/npm/anyformer@0.4.4/+esm';` |
18
18
  | Copy | `index.js` and `src/`; nothing else is needed |
19
19
 
20
20
  ```js
@@ -59,7 +59,7 @@ back end's answer: yes with an estimate (GPU bytes, disk bytes, tok/s when measu
59
59
  | `localmind` | LocalMind's hand-written WebGPU kernels (shipped in diskformer), the whole model on the GPU | any LFM2 / LFM2.5 GGUF; Gemma 4 E2B / E4B QAT-mobile; Ternary Bonsai 2 |
60
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
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, Flint 40B, LFM2.5 24B (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 |
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…), split sets included; a hand engine above wins where it covers the model |
63
63
 
64
64
  Each runtime loads from an exact version or commit in [src/sources.js](src/sources.js); set a back end's `.source`
65
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyformer",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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",
@@ -30,22 +30,43 @@ export const CHECKED_FILES = [
30
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
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
32
  'Ling-3.0-tiny-Q4_K_M.gguf', 'flint-int4-q4_1.gguf', 'OLMoE-1B-7B-0125-Instruct-Q4_K_M.gguf',
33
+ 'Ling-3.0-flash-Q4_K_M-00001-of-00002.gguf',
33
34
  ];
34
35
 
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.
36
+ const SPLIT = /-(\d{5})-of-(\d{5})\.gguf$/i;
37
+
38
+ // The GGUF to load, as { name, size, parts: [{ name, size }] }: the language model (no mmproj vision tower), one file or
39
+ // every part of a split set (name-00001-of-0000n.gguf, named by its first part; size is the sum). The caller's file
40
+ // when given; else the file diskformer checked; else the preferred quant, a single file before a split set.
37
41
  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()));
42
+ const lms = model.files.gguf.filter((f) => !/mmproj/i.test(f.name));
43
+ const sets = new Map();
44
+ for (const f of lms.filter((x) => SPLIT.test(x.name))) {
45
+ const base = f.name.replace(SPLIT, '');
46
+ sets.set(base, [...(sets.get(base) || []), f]);
47
+ }
48
+ const candidates = [
49
+ ...lms.filter((f) => !SPLIT.test(f.name)).map((f) => ({ name: f.name, size: f.size, parts: [f] })),
50
+ // A set missing a part cannot load.
51
+ ...[...sets.values()].filter((p) => p.length === Number(SPLIT.exec(p[0].name)[2])).map((p) => {
52
+ const parts = [...p].sort((a, b) => a.name.localeCompare(b.name));
53
+ return { name: parts[0].name, size: parts.reduce((n, f) => n + f.size, 0), parts };
54
+ }),
55
+ ];
56
+ if (file) return candidates.find((f) => f.name === file) || null;
57
+ const checked = candidates.find((f) => CHECKED_FILES.includes(opfsName(f)));
41
58
  if (checked) return checked;
42
59
  for (const q of QUANTS) {
43
- const f = lms.find((x) => new RegExp(`(^|[-_.])${q}([-_.]|$)`, 'i').test(x.name.replace(/\.gguf$/i, '')));
60
+ const f = candidates.find((x) => new RegExp(`(^|[-_.])${q}([-_.]|$)`, 'i').test(x.name.replace(SPLIT, '').replace(/\.gguf$/i, '')));
44
61
  if (f) return f;
45
62
  }
46
63
  return null;
47
64
  }
48
65
 
66
+ // llama.cpp's GGUFs sit in OPFS as gguf/<file name>, without the repo's folders: the worker finds a split set's other
67
+ // parts by name beside the first.
68
+ const opfsName = (f) => f.name.split('/').pop();
69
+
49
70
  // OPFS keeps llama.cpp's GGUFs by file name (gguf/<name>), with no repo. anyformer records name → repo here, so
50
71
  // stored() can say which repo a file belongs to.
51
72
  export const LLAMA_INDEX = 'anyformer-llama.json';
@@ -82,7 +103,7 @@ export const diskformerLlamaBackend = {
82
103
  if (task && !diskformerLlamaBackend.tasks.includes(task)) return { ok: false, code: 'task', reason: `task ${task}; diskformer's llama.cpp path runs text generation only` };
83
104
  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
105
  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` };
106
+ if (!f) return { ok: false, code: 'model', reason: file ? `${file} is not a language-model GGUF in the repo (a split set is named by its first part, with every part present)` : `no GGUF at ${QUANTS.join(', ')} in the repo (a split set needs every part)` };
86
107
  if (!caps.webgpu) return { ok: false, code: 'webgpu', reason: 'no WebGPU adapter' };
87
108
  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
109
  if (!caps.opfs) return { ok: false, code: 'opfs', reason: 'no OPFS (navigator.storage.getDirectory)' };
@@ -100,16 +121,22 @@ export const diskformerLlamaBackend = {
100
121
  signal?.addEventListener('abort', stop, { once: true });
101
122
  let info;
102
123
  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();
124
+ // Each part downloads under its own name; progress counts bytes over the whole set.
125
+ let before = 0;
126
+ for (const part of f.parts) {
127
+ const name = opfsName(part);
128
+ const have = (await llama.list()).find((m) => m.name === name && !m.partial);
129
+ if (!have || have.size !== part.size) {
130
+ say({ status: 'download', loaded: before, total: f.size });
131
+ await llama.download({ url: diskformerLlamaBackend.url(model, part), name, size: part.size },
132
+ (p) => say({ status: 'download', loaded: before + p.copied, total: f.size }));
133
+ signal?.throwIfAborted();
134
+ }
135
+ before += part.size;
109
136
  }
110
- await writeIndex({ ...(await readIndex()), [f.name]: model.repo });
137
+ await writeIndex({ ...(await readIndex()), ...Object.fromEntries(f.parts.map((p) => [opfsName(p), model.repo])) });
111
138
  say({ status: 'load' });
112
- info = await llama.load({ name: f.name, budget, ctx: maxCtx }, (p) => say({ status: 'plan', plan: p }));
139
+ info = await llama.load({ name: opfsName(f), budget, ctx: maxCtx }, (p) => say({ status: 'plan', plan: p }));
113
140
  signal?.throwIfAborted();
114
141
  } catch (e) {
115
142
  llama.terminate();
package/src/sources.js CHANGED
@@ -7,9 +7,10 @@ export const PINS = {
7
7
  transformers: '4.3.1',
8
8
  wllama: '3.4.1', // LocalMind's pin; its stream-drain workaround (src/backends/wllama.js) is for this version
9
9
  kohra: 'f678fac7342c79b58b640540b8b3a7d078b7ab1a',
10
- // v0.6.4: every engine (the disk-tier MoE engines, Ling 3.0 Flash, and LocalMind's whole-model engines LFM2.5,
11
- // Gemma 4 E2B/E4B and Ternary Bonsai 2) under engines/, and the llama.cpp path under ggml/.
12
- diskformer: '095ab2058be9de56841c51b9369ba556d625392b',
10
+ // v0.6.7: every engine (the disk-tier MoE engines, Ling 3.0 Flash, and LocalMind's whole-model engines LFM2.5,
11
+ // Gemma 4 E2B/E4B and Ternary Bonsai 2) under engines/, and the llama.cpp path under ggml/, which loads split GGUFs.
12
+ // The hand engines' ingest streams the GGUF header with progress and resumes a dropped range.
13
+ diskformer: '663b2b01cfbed95ae8d319ce845d8aea759d60cf',
13
14
  };
14
15
 
15
16
  const gh = (repo, sha, path) => `https://cdn.jsdelivr.net/gh/NakliTechie/${repo}@${sha}/${path}`;