anyformer 0.4.3 → 0.4.6

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,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.6 — 2026-10-24
4
+
5
+ Skips 0.4.5 on npm: this release carries its diskformer v0.6.8 pin too.
6
+
7
+ ### Added
8
+ - Nemotron 3.5 Bolt E4B (`MODELS.chat.NEMOTRON_3_5_BOLT_E4B`) on `transformers` at a 5 GB budget:
9
+ danyalmalik/NVIDIA-Nemotron-3.5-Bolt-E4B-ONNX-WebGPU, a community q4f16 export (Bolt's weights in onnx-community's
10
+ Gemma 4 E4B graph), text only, 4.91 GB.
11
+ - `transformers`: a Gemma 4 repo in the multimodal layout with no vision or audio encoder loads through
12
+ `Gemma4ForCausalLM` and the tokenizer. Its `use_external_data_format` comes from the repo's `_data` files, because
13
+ that repo's config.json has no transformers.js_config (without it: "Module.MountedFiles is not available"). Image
14
+ and audio blocks are refused with `INVALID_ARGUMENT`.
15
+
16
+ ### Verified
17
+ - Browser (Chrome, M4 Pro): `npm run test:browser -- transformers --query "repo=danyalmalik/NVIDIA-Nemotron-3.5-Bolt-E4B-ONNX-WebGPU&budget=5.3"`
18
+ answered on topic, 34.9 tok/s, load 231 s with the download. npm test 88/88.
19
+
20
+ ## 0.4.5 — 2026-10-24
21
+
22
+ ### Changed
23
+ - diskformer pinned to v0.6.8 (3aad103). Its llama.cpp path (ggml/) reads past 2 GiB in a browser, adds head-512
24
+ attention and loads drafter GGUFs (`draft`). The hand engines (engines/, src/) are unchanged since v0.6.7.
25
+
26
+ ### Verified
27
+ - `llama-gate` on Granite 4.0 H Tiny (Chrome, M4 Pro, v0.6.8 pin): 9/9 identical to diskformer's refs, chat path
28
+ equal, 19.1 tok/s median. npm test 87/87. Not rerun: the hand-engine gates, because engines/ did not change.
29
+
30
+ ## 0.4.4 — 2026-10-24
31
+
32
+ ### Changed
33
+ - diskformer pinned to v0.6.7 (663b2b0). Its hand engines' ingest (src/ingest.js) now streams the GGUF header with
34
+ progress, so a slow link no longer goes silent for minutes (Flint at 0.1 MB/s stalled LocalMind's watchdog for 3 min).
35
+ A range that drops mid-stream, or returns 5xx or 429, resumes from the last byte, up to 6 times. The new events reach
36
+ `onProgress` as status `weights` with phase `header` or `retry`. diskformer changed only src/ingest.js and its test
37
+ between v0.6.6 and v0.6.7.
38
+
39
+ ### Verified
40
+ - npm test 87/87. Not run here in a browser: LocalMind's throttled production load of Flint is that check.
41
+
3
42
  ## 0.4.3 — 2026-10-24
4
43
 
5
44
  npm skips 0.4.2: this release carries it (Flint 40B and LFM2.5 24B on the hand engines).
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.3/+esm';` |
17
+ | CDN | `import { load } from 'https://cdn.jsdelivr.net/npm/anyformer@0.4.6/+esm';` |
18
18
  | Copy | `index.js` and `src/`; nothing else is needed |
19
19
 
20
20
  ```js
@@ -54,7 +54,7 @@ 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 |
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
58
  | `kohra` | kohra, text-diffusion over onnxruntime-web | MDLM and BD3LM repos tagged `text-diffusion` |
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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anyformer",
3
- "version": "0.4.3",
3
+ "version": "0.4.6",
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",
@@ -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
@@ -17,6 +17,7 @@ const GEMMA4_E4B_ONNX = M('onnx-community/gemma-4-E4B-it-ONNX', 'transformers',
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
19
  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 });
20
+ 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
21
  const MINILM_L6 = M('Xenova/all-MiniLM-L6-v2', 'transformers', 'feature-extraction', 'embed', { downloadBytes: 90e6 });
21
22
  const DETR_RESNET_50 = M('Xenova/detr-resnet-50', 'transformers', 'object-detection', 'detect', { downloadBytes: 167e6 });
22
23
 
@@ -42,6 +43,8 @@ export const MODELS = {
42
43
  QWEN3_0_6B_MDLM,
43
44
  /** 26B parameters in 2.5 GB of GPU memory; 14.4 GB on disk (diskformer). */
44
45
  GEMMA4_26B_A4B,
46
+ /** NVIDIA's Nemotron 3.5 Bolt E4B (dense, text only), a community q4f16 ONNX export in Gemma 4 E4B's graph. */
47
+ NEMOTRON_3_5_BOLT_E4B,
45
48
  },
46
49
  /** Chat with images in the messages: content [{ type: 'image', image }, { type: 'text', text }]. Gemma 4 takes audio too. */
47
50
  vision: { DEFAULT: QWEN3_5_4B, QWEN3_5_4B, GEMMA4_E2B_ONNX, GEMMA4_E4B_ONNX },
package/src/sources.js CHANGED
@@ -7,9 +7,11 @@ 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.6: 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
- diskformer: 'e07e12975df054b073672ba2060d0d2c5571dbcd',
10
+ // v0.6.8: 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
+ // and drafters, and reads past 2 GiB (Nemotron 3.5 Bolt E4B needs v0.6.8 or later). The hand engines' ingest
13
+ // streams the GGUF header with progress and resumes a dropped range.
14
+ diskformer: '3aad103e47cac76c1471e92a438edfe1c80dd86e',
13
15
  };
14
16
 
15
17
  const gh = (repo, sha, path) => `https://cdn.jsdelivr.net/gh/NakliTechie/${repo}@${sha}/${path}`;