anyformer 0.4.4 → 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 +27 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/backends/transformers.js +42 -0
- package/src/models.js +3 -0
- package/src/sources.js +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
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
|
+
|
|
3
30
|
## 0.4.4 — 2026-10-24
|
|
4
31
|
|
|
5
32
|
### 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.
|
|
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
|
+
"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,10 +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.
|
|
11
|
-
// Gemma 4 E2B/E4B and Ternary Bonsai 2) under engines/, and the llama.cpp path under ggml/, which loads split GGUFs
|
|
12
|
-
//
|
|
13
|
-
|
|
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',
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
const gh = (repo, sha, path) => `https://cdn.jsdelivr.net/gh/NakliTechie/${repo}@${sha}/${path}`;
|