anyformer 0.4.4 → 0.4.8

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,63 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.8 — 2026-10-24
4
+
5
+ Skips 0.4.7 on npm: this release carries its Nemotron-Labs-Diffusion-3B entry too.
6
+
7
+ ### Changed
8
+ - kohra pinned to v0.3.0 (85e01f0); kohra.js is identical to 0.4.7's pin, 14e430c.
9
+ - Nemotron-Labs-Diffusion-3B decodes with threshold 0.9 by default (Fast-dLLM: unmask every position at that
10
+ confidence per step); `generate(messages, { threshold: null })` runs fixed steps. kohra measured 34 forward passes
11
+ against 64 with the same answer.
12
+
13
+ ### Verified
14
+ - Browser (Chrome, M4 Pro, GPU shared with another session): one page, the CPU prompt, 32 tokens, threshold
15
+ null / 0.9 / null / 0.9: identical text in every run; 7.1 s and 5.8 s at fixed steps, 4.9 s at 0.9. npm test 89/89.
16
+
17
+ ## 0.4.7 — 2026-10-24
18
+
19
+ ### Added
20
+ - Nemotron-Labs-Diffusion-3B (`MODELS.chat.NEMOTRON_DIFFUSION_3B`, naklitechie/Nemotron-Labs-Diffusion-3B-ONNX) on
21
+ `kohra`: the q4f16 seeded block-KV graph (onnx/model_kv_q4f16.onnx, 2.73 GB with its two data files), mask id 100,
22
+ EOS 11, and Nemotron's chat template over the whole conversation (a system turn, an empty think block before each
23
+ assistant reply). Content blocks are joined as text.
24
+
25
+ ### Changed
26
+ - kohra pinned to main 14e430c (after v0.2.1; kohra.js changed in afee5f4): seeded KV graphs, `chatFormat`, graphs
27
+ whose data is split into `_data`, `_data_1`.
28
+
29
+ ### Verified
30
+ - Browser (Chrome, M4 Pro): `npm run test:browser -- kohra --query "repo=naklitechie/Nemotron-Labs-Diffusion-3B-ONNX"`
31
+ answered on topic (32 tokens, 5.0 tok/s, load 156 s with the download); `npm run test:browser -- kohra` (Qwen3-0.6B
32
+ MDLM) on the new pin answered on topic. npm test 89/89.
33
+
34
+ ## 0.4.6 — 2026-10-24
35
+
36
+ Skips 0.4.5 on npm: this release carries its diskformer v0.6.8 pin too.
37
+
38
+ ### Added
39
+ - Nemotron 3.5 Bolt E4B (`MODELS.chat.NEMOTRON_3_5_BOLT_E4B`) on `transformers` at a 5 GB budget:
40
+ danyalmalik/NVIDIA-Nemotron-3.5-Bolt-E4B-ONNX-WebGPU, a community q4f16 export (Bolt's weights in onnx-community's
41
+ Gemma 4 E4B graph), text only, 4.91 GB.
42
+ - `transformers`: a Gemma 4 repo in the multimodal layout with no vision or audio encoder loads through
43
+ `Gemma4ForCausalLM` and the tokenizer. Its `use_external_data_format` comes from the repo's `_data` files, because
44
+ that repo's config.json has no transformers.js_config (without it: "Module.MountedFiles is not available"). Image
45
+ and audio blocks are refused with `INVALID_ARGUMENT`.
46
+
47
+ ### Verified
48
+ - Browser (Chrome, M4 Pro): `npm run test:browser -- transformers --query "repo=danyalmalik/NVIDIA-Nemotron-3.5-Bolt-E4B-ONNX-WebGPU&budget=5.3"`
49
+ answered on topic, 34.9 tok/s, load 231 s with the download. npm test 88/88.
50
+
51
+ ## 0.4.5 — 2026-10-24
52
+
53
+ ### Changed
54
+ - diskformer pinned to v0.6.8 (3aad103). Its llama.cpp path (ggml/) reads past 2 GiB in a browser, adds head-512
55
+ attention and loads drafter GGUFs (`draft`). The hand engines (engines/, src/) are unchanged since v0.6.7.
56
+
57
+ ### Verified
58
+ - `llama-gate` on Granite 4.0 H Tiny (Chrome, M4 Pro, v0.6.8 pin): 9/9 identical to diskformer's refs, chat path
59
+ equal, 19.1 tok/s median. npm test 87/87. Not rerun: the hand-engine gates, because engines/ did not change.
60
+
3
61
  ## 0.4.4 — 2026-10-24
4
62
 
5
63
  ### Changed
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.4.4/+esm';` |
17
+ | CDN | `import { load } from 'https://cdn.jsdelivr.net/npm/anyformer@0.4.8/+esm';` |
18
18
  | Copy | `index.js` and `src/`; nothing else is needed |
19
19
 
20
20
  ```js
@@ -54,8 +54,8 @@ back end's answer: yes with an estimate (GPU bytes, disk bytes, tok/s when measu
54
54
 
55
55
  | Back end | Runs | Models |
56
56
  |---|---|---|
57
- | `transformers` | transformers.js 4.3.1: text generation, image and audio chat (Gemma 4, Qwen3.5), embeddings, speech to text, detection, classification, depth, segmentation | ONNX repos in the onnx-community / Xenova layout |
58
- | `kohra` | kohra, text-diffusion over onnxruntime-web | MDLM and BD3LM repos tagged `text-diffusion` |
57
+ | `transformers` | transformers.js 4.3.1: text generation (Nemotron 3.5 Bolt E4B among them), image and audio chat (Gemma 4, Qwen3.5), embeddings, speech to text, detection, classification, depth, segmentation | ONNX repos in the onnx-community / Xenova layout |
58
+ | `kohra` | kohra, text-diffusion over onnxruntime-web | MDLM and BD3LM repos tagged `text-diffusion`; Nemotron-Labs-Diffusion-3B |
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) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyformer",
3
- "version": "0.4.4",
3
+ "version": "0.4.8",
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",
@@ -9,8 +9,29 @@ const FP16 = 'onnx/model_fp16_fused.onnx';
9
9
  const Q4 = 'onnx/model_q4f16_rtn_sym.onnx';
10
10
  const Q4_ORT = '1.26.0-dev.20260416-b7804b056c';
11
11
 
12
+ // Models with their own graph and tokens, keyed by config.model_type (kohra index.html REGISTRY at the pinned commit).
13
+ // Nemotron-Labs-Diffusion-3B: a seeded block-KV graph (kohra.js finds its attention_mask input and denoises blocks
14
+ // from the prompt end), mask id 100, EOS 11, and Nemotron's template: a system turn, always, and an empty think block
15
+ // before each assistant reply.
16
+ const SPECIAL = {
17
+ nemotron_labs_diffusion: {
18
+ graph: 'onnx/model_kv_q4f16.onnx', opts: { kvCache: true, maskId: 100n, eosId: 11n }, blockCausal: false,
19
+ // Fast-dLLM decoding: unmask every position at confidence 0.9 or more per step. Same answer in 34 forward passes
20
+ // where fixed steps took 64 (kohra, primes prompt).
21
+ threshold: 0.9,
22
+ template: (messages) => {
23
+ const [system, rest] = messages[0]?.role === 'system' ? [messages[0].content, messages.slice(1)] : ['', messages];
24
+ return `<|im_start|>system\n${system}<|im_end|>\n` + rest.map((m) => (m.role === 'assistant'
25
+ ? `<|im_start|>assistant\n<think></think>${m.content.trim()}<|im_end|>\n`
26
+ : `<|im_start|>${m.role}\n${m.content}<|im_end|>\n`)).join('') + '<|im_start|>assistant\n<think></think>';
27
+ },
28
+ },
29
+ };
30
+
12
31
  const pickGraph = (model, caps) => {
13
32
  const have = new Set(model.files.onnx.map((f) => f.name));
33
+ const special = SPECIAL[model.architecture];
34
+ if (special) return have.has(special.graph) ? special.graph : null;
14
35
  return caps.f16 && have.has(FP16) ? FP16 : have.has(Q4) ? Q4 : null;
15
36
  };
16
37
 
@@ -18,6 +39,9 @@ const pickGraph = (model, caps) => {
18
39
  const chatml = (messages) =>
19
40
  messages.map((m) => `<|im_start|>${m.role}\n${m.content}<|im_end|>\n`).join('') + '<|im_start|>assistant\n';
20
41
 
42
+ // Text blocks joined: kohra reads text only.
43
+ const text = (messages) => messages.map((m) => (typeof m.content === 'string' ? m : { ...m, content: m.content.map((b) => b.text ?? '').join('\n') }));
44
+
21
45
  export const kohraBackend = {
22
46
  id: 'kohra',
23
47
  tasks: ['text-generation'],
@@ -28,7 +52,7 @@ export const kohraBackend = {
28
52
  if (task && task !== 'text-generation') return { ok: false, code: 'task', reason: `task ${task}; kohra runs text-generation only` };
29
53
  if (!caps.webgpu) return { ok: false, code: 'webgpu', reason: 'no WebGPU adapter' };
30
54
  const graph = pickGraph(model, caps);
31
- if (!graph) return { ok: false, code: 'format', reason: `no kohra graph (${FP16} or ${Q4}) in the repo` };
55
+ if (!graph) return { ok: false, code: 'format', reason: `no kohra graph (${SPECIAL[model.architecture]?.graph || `${FP16} or ${Q4}`}) in the repo` };
32
56
  const bytes = graphBytes(model, graph);
33
57
  return { ok: true, estimate: { gpuBytes: bytes, diskBytes: bytes, tokPerSec: null } };
34
58
  },
@@ -38,18 +62,21 @@ export const kohraBackend = {
38
62
  const { DiffusionLM } = await import(kohraBackend.source);
39
63
  onProgress?.({ backend: 'kohra', status: 'loading', file: graph });
40
64
  const base = `https://huggingface.co/${model.repo}/resolve/${model.revision || 'main'}`;
65
+ const special = SPECIAL[model.architecture];
41
66
  const lm = await DiffusionLM.from_pretrained({
42
- model: `${base}/${graph}`, tokenizer: model.repo, ...(graph === Q4 && { ortVersion: Q4_ORT }),
67
+ model: `${base}/${graph}`, tokenizer: model.repo, ...(graph === Q4 && { ortVersion: Q4_ORT }), ...special?.opts,
43
68
  });
44
69
  signal?.throwIfAborted();
45
- const blockCausal = model.tags.includes('block-diffusion');
70
+ const blockCausal = special ? special.blockCausal : model.tags.includes('block-diffusion');
71
+ const prompt = special?.template || chatml;
46
72
  return {
47
73
  task: 'text-generation',
48
74
  // Diffusion fills the whole block at once, so the reply arrives as one chunk.
49
- async *generate(messages, { maxNewTokens = 128, steps, signal } = {}) {
75
+ // threshold: unmask every position at that confidence per step (Fast-dLLM); null runs fixed steps.
76
+ async *generate(messages, { maxNewTokens = 128, steps, threshold = special?.threshold ?? null, signal } = {}) {
50
77
  signal?.throwIfAborted();
51
- const out = await lm.generate(chatml(messages), {
52
- maxNewTokens, steps: steps ?? maxNewTokens, blockCausal, chatTemplate: false, stripThink: true,
78
+ const out = await lm.generate(prompt(text(messages)), {
79
+ maxNewTokens, steps: steps ?? maxNewTokens, threshold, blockCausal, chatTemplate: false, stripThink: true,
53
80
  });
54
81
  yield { text: out.text, tokens: out.tokens, tokPerSec: out.tokensPerSecond };
55
82
  },
@@ -2,6 +2,7 @@
2
2
  // importing anyformer costs nothing until this back end is chosen.
3
3
 
4
4
 
5
+ import { AnyformerError } from '../errors.js';
5
6
  import { SOURCES } from '../sources.js';
6
7
 
7
8
  // dtype → the ONNX file transformers.js reads for it (decoder-only repos keep one graph per dtype under onnx/).
@@ -31,6 +32,18 @@ function pickMmDtype(model, caps) {
31
32
  return order.find((d) => have.has(`onnx/decoder_model_merged${MM_SUFFIX[d]}.onnx`)) || null;
32
33
  }
33
34
  const mmBytes = (model, dtype) => mmModules(model).reduce((n, m) => n + graphBytes(model, `onnx/${m}${MM_SUFFIX[dtype]}.onnx`), 0);
35
+ // A text-only export in the multimodal layout (embed_tokens + decoder_model_merged, no vision or audio encoder), e.g.
36
+ // danyalmalik/NVIDIA-Nemotron-3.5-Bolt-E4B-ONNX-WebGPU: loads through the causal-LM class and the tokenizer, as
37
+ // LocalMind does, since there is no processor.
38
+ const CAUSAL = { gemma4: 'Gemma4ForCausalLM' };
39
+ const textOnly = (model) => model.architecture in CAUSAL && !mmModules(model).some((m) => /^(vision|audio)_encoder$/.test(m));
40
+ // How many external-data files each graph has (<graph>_data, <graph>_data_1 …), keyed as transformers.js_config's
41
+ // use_external_data_format is: a repo whose config.json lacks that block fails to mount the data without it. The
42
+ // counts equal the config's in onnx-community's Gemma 4 E2B and Qwen3.5 4B repos.
43
+ const externalData = (model, dtype) => Object.fromEntries(mmModules(model).map((m) => {
44
+ const file = `onnx/${m}${MM_SUFFIX[dtype]}.onnx`;
45
+ return [file.slice(5), model.files.all.filter((f) => f.name.startsWith(`${file}_data`)).length];
46
+ }).filter(([, n]) => n > 0));
34
47
  const isMultimodal = (model) => model.architecture in MULTIMODAL && model.files.onnx.some((f) => f.name.startsWith('onnx/decoder_model_merged'));
35
48
  // The chat tasks a multimodal repo is tagged with on the Hub; all run here as chat.
36
49
  const MM_TASKS = ['text-generation', 'image-text-to-text', 'any-to-any'];
@@ -100,6 +113,7 @@ export const transformersBackend = {
100
113
  const want = task || model.task || 'text-generation';
101
114
  const lib = await import(transformersBackend.source);
102
115
  const progress_callback = onProgress && ((p) => onProgress({ backend: 'transformers', ...p }));
116
+ if (isMultimodal(model) && textOnly(model)) return loadCausal(lib, model, forced || pickMmDtype(model, caps), { progress_callback, signal });
103
117
  if (isMultimodal(model)) return loadMultimodal(lib, model, forced || pickMmDtype(model, caps), { progress_callback, signal });
104
118
  if (want !== 'text-generation') return loadPipeline(lib, model, want, { progress_callback, signal });
105
119
  const dtype = forced || pickDtype(model, caps);
@@ -156,6 +170,34 @@ async function* stream(lib, tokenizer, signal, run) {
156
170
  if (failed) throw failed;
157
171
  }
158
172
 
173
+ // A text-only model in the multimodal layout: the causal-LM class over embed_tokens + decoder_model_merged.
174
+ async function loadCausal(lib, model, dtype, { progress_callback, signal }) {
175
+ const opts = { revision: model.revision || 'main', progress_callback };
176
+ const tokenizer = await lib.AutoTokenizer.from_pretrained(model.repo, opts);
177
+ const net = await lib[CAUSAL[model.architecture]].from_pretrained(model.repo, {
178
+ ...opts, dtype, device: 'webgpu', use_external_data_format: externalData(model, dtype),
179
+ });
180
+ signal?.throwIfAborted();
181
+ return {
182
+ task: 'text-generation',
183
+ generate: (messages, { maxNewTokens = 256, repetitionPenalty, signal } = {}) => stream(lib, tokenizer, signal, (streamer, stopping_criteria) => {
184
+ const inputs = tokenizer.apply_chat_template(textMessages(messages), { add_generation_prompt: true, enable_thinking: false, return_dict: true });
185
+ return net.generate({ ...inputs, max_new_tokens: maxNewTokens, do_sample: false, ...(repetitionPenalty && { repetition_penalty: repetitionPenalty }), streamer, stopping_criteria });
186
+ }),
187
+ dispose: () => net.dispose?.(),
188
+ };
189
+ }
190
+
191
+ // Text blocks joined; a text-only model refuses any other block.
192
+ function textMessages(messages) {
193
+ return messages.map((m) => {
194
+ if (typeof m.content === 'string') return m;
195
+ const other = m.content.find((b) => b.type !== 'text');
196
+ if (other) throw new AnyformerError('INVALID_ARGUMENT', `this model reads text only; got a ${other.type} block`);
197
+ return { ...m, content: m.content.map((b) => b.text).join('\n') };
198
+ });
199
+ }
200
+
159
201
  // A multimodal chat model. Message content is a string or an array of blocks: { type: 'text', text },
160
202
  // { type: 'image', image } (ImageData, RawImage or a URL) and, for Gemma 4, { type: 'audio', audio } (16 kHz
161
203
  // Float32Array). The template sees placeholders; the processor gets the media, in order.
package/src/models.js CHANGED
@@ -16,7 +16,9 @@ const GEMMA4_E2B_ONNX = M('onnx-community/gemma-4-E2B-it-ONNX', 'transformers',
16
16
  const GEMMA4_E4B_ONNX = M('onnx-community/gemma-4-E4B-it-ONNX', 'transformers', 'text-generation', 'transformers', { budget: 5.3e9, downloadBytes: 5.18e9 });
17
17
  const MINICPM5_2B = M('openbmb/MiniCPM5-2B-GGUF', 'wllama', 'text-generation', 'chat', { downloadBytes: 1.56e9 });
18
18
  const QWEN3_0_6B_MDLM = M('naklitechie/Qwen3-0.6B-diffusion-mdlm-ONNX', 'kohra', 'text-generation', 'kohra', { downloadBytes: 1.54e9 });
19
+ const NEMOTRON_DIFFUSION_3B = M('naklitechie/Nemotron-Labs-Diffusion-3B-ONNX', 'kohra', 'text-generation', 'kohra', { downloadBytes: 2.77e9 });
19
20
  const GEMMA4_26B_A4B = M('google/gemma-4-26B-A4B-it-qat-q4_0-gguf', 'diskformer', 'text-generation', 'diskformer-chat', { budget: 2.5e9, downloadBytes: 14.44e9 });
21
+ const NEMOTRON_3_5_BOLT_E4B = M('danyalmalik/NVIDIA-Nemotron-3.5-Bolt-E4B-ONNX-WebGPU', 'transformers', 'text-generation', 'transformers', { budget: 5e9, downloadBytes: 4.94e9 });
20
22
  const MINILM_L6 = M('Xenova/all-MiniLM-L6-v2', 'transformers', 'feature-extraction', 'embed', { downloadBytes: 90e6 });
21
23
  const DETR_RESNET_50 = M('Xenova/detr-resnet-50', 'transformers', 'object-detection', 'detect', { downloadBytes: 167e6 });
22
24
 
@@ -40,8 +42,12 @@ export const MODELS = {
40
42
  MINICPM5_2B,
41
43
  /** Text diffusion: the whole reply denoises at once (kohra). */
42
44
  QWEN3_0_6B_MDLM,
45
+ /** NVIDIA's Nemotron-Labs-Diffusion-3B, block diffusion with a KV cache, q4 (kohra). */
46
+ NEMOTRON_DIFFUSION_3B,
43
47
  /** 26B parameters in 2.5 GB of GPU memory; 14.4 GB on disk (diskformer). */
44
48
  GEMMA4_26B_A4B,
49
+ /** NVIDIA's Nemotron 3.5 Bolt E4B (dense, text only), a community q4f16 ONNX export in Gemma 4 E4B's graph. */
50
+ NEMOTRON_3_5_BOLT_E4B,
45
51
  },
46
52
  /** Chat with images in the messages: content [{ type: 'image', image }, { type: 'text', text }]. Gemma 4 takes audio too. */
47
53
  vision: { DEFAULT: QWEN3_5_4B, QWEN3_5_4B, GEMMA4_E2B_ONNX, GEMMA4_E4B_ONNX },
package/src/sources.js CHANGED
@@ -6,11 +6,13 @@
6
6
  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
- kohra: 'f678fac7342c79b58b640540b8b3a7d078b7ab1a',
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',
9
+ // v0.3.0: seeded block-KV graphs (Nemotron-Labs-Diffusion-3B), chatFormat, split external data.
10
+ kohra: '85e01f0193163b0f2e9696d4f6f5e12b68a277e9',
11
+ // v0.6.8: every engine (the disk-tier MoE engines, Ling 3.0 Flash, and LocalMind's whole-model engines LFM2.5,
12
+ // Gemma 4 E2B/E4B and Ternary Bonsai 2) under engines/, and the llama.cpp path under ggml/, which loads split GGUFs
13
+ // and drafters, and reads past 2 GiB (Nemotron 3.5 Bolt E4B needs v0.6.8 or later). The hand engines' ingest
14
+ // streams the GGUF header with progress and resumes a dropped range.
15
+ diskformer: '3aad103e47cac76c1471e92a438edfe1c80dd86e',
14
16
  };
15
17
 
16
18
  const gh = (repo, sha, path) => `https://cdn.jsdelivr.net/gh/NakliTechie/${repo}@${sha}/${path}`;