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 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.20",
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
- onProgress?.({ backend: 'diskformer', status: 'adapter', file: adapter.file });
139
- lora = await engine.loadAdapter(diskformerBackend.adapterUrl(adapter), { scale: adapter.scale ?? 1 });
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: 'cefebc7c18bf106f82378925bd5650cae4320193',
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): one entry per file URL
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