anyformer 0.4.21 → 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,22 @@
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
+
3
20
  ## 0.4.21 — 2026-10-31
4
21
 
5
22
  ### Changed
@@ -9,8 +26,13 @@
9
26
  9/9, 6/9. Adapters on routed experts are refused with an error. MiniCPM-V, Clover, Flint and Ling take no adapter,
10
27
  so an `adapter` drops `diskformer` from their plans.
11
28
 
12
- ### Not yet verified
13
- - No browser run of an adapter on Gemma 4 or Qwen3.6 through anyformer: no adapter GGUF for either is in the catalog.
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).
14
36
 
15
37
  ## 0.4.20 — 2026-10-31
16
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyformer",
3
- "version": "0.4.21",
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",
@@ -81,6 +81,41 @@ function budgetError(e) {
81
81
  return m && new AnyformerError('BUDGET_TOO_SMALL', `diskformer: ${e.message}`, { cause: e, need: Math.ceil((+m[1] + +m[2]) * 1e9) });
82
82
  }
83
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
+
84
119
  const has = (model, file, size) => model.files.gguf.some((f) => f.name === file && f.size === size);
85
120
  const match = (model) => DISKFORMER_MODELS.find((m) => m.repo === model.repo &&
86
121
  (m.parts ? m.parts.every((p) => has(model, p.file, p.size)) : has(model, m.file, m.size)));
@@ -121,7 +156,8 @@ export const diskformerBackend = {
121
156
  // its pool from gpuBudgetBytes when given one, so it gets one or the other).
122
157
  // localFile: the GGUF as a File the user picked; the engine ingests it instead of downloading, and refuses a file
123
158
  // whose size differs from the one it is pinned to.
124
- // 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);
125
161
  // session.setAdapter(false / true) switches it off and on again with no reload.
126
162
  async load(model, { budget, maxCtx = 1024, poolBytes, localFile, adapter = null, onProgress, signal }) {
127
163
  const m = match(model);
@@ -139,8 +175,8 @@ export const diskformerBackend = {
139
175
  let lora = null;
140
176
  if (adapter) {
141
177
  try {
142
- onProgress?.({ backend: 'diskformer', status: 'adapter', file: adapter.file });
143
- 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 });
144
180
  await engine.setAdapter(lora);
145
181
  } catch (e) { await engine.dispose?.(); throw e; }
146
182
  }
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