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 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.17",
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
- async load(model, { budget, file = null, maxCtx = 1024, onProgress, signal }) {
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 and side file downloads under its own name; progress counts bytes over all of them.
144
- const files = [...f.parts, ...f.extra];
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 part of files) {
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 !== part.size) {
150
- say({ status: 'download', loaded: before, total: f.size });
151
- await llama.download({ url: diskformerLlamaBackend.url(model, part), name, size: part.size },
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 += part.size;
161
+ before += size;
156
162
  }
157
- await writeIndex({ ...(await readIndex()), ...Object.fromEntries(files.map((p) => [opfsName(p), model.repo])) });
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) }, (p) => say({ status: 'plan', plan: p }));
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 budget apply unless opts set them).
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
- return { model, caps, budget, task, file, ...(await resolve(model, caps, backends, { budget, task, file })) };
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: 'f17009cc31e15361af7ae5536e8f55cb43c5066f',
33
+ diskformer: '317d2a002d95278e504ed84fab2d0f71dabaa884',
27
34
  };
28
35
 
29
36
  const gh = (repo, sha, path) => `https://cdn.jsdelivr.net/gh/NakliTechie/${repo}@${sha}/${path}`;