@degreesign/chat 1.5.1 → 1.7.0
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/README.md +278 -618
- package/dist/browser/chat.min.mjs +1 -1
- package/dist/deprecated.d.ts +10 -0
- package/dist/describe.d.ts +16 -17
- package/dist/engines/wllama.d.ts +11 -1
- package/dist/enums.d.ts +22 -1
- package/dist/estimate.d.ts +13 -0
- package/dist/index.d.ts +4 -3
- package/dist/node/chat.node.min.js +1 -1
- package/dist/node/chat.node.min.mjs +1 -1
- package/dist/prompts.d.ts +18 -0
- package/dist/types.d.ts +95 -6
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
# @degreesign/chat
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
engine and a LiteRT-LM WebGPU engine
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Voice notes are transcribed on device by `@degreesign/voice`, a companion
|
|
8
|
-
package, then passed to `@degreesign/chat` as text.
|
|
9
|
-
|
|
10
|
-
`@degreesign/chat` is a plug and play engine for any UI.
|
|
3
|
+
Headless on-device AI chat for browsers, workers and Node: one model in memory,
|
|
4
|
+
one streamed reply, no UI. A wllama CPU engine and a LiteRT-LM WebGPU engine
|
|
5
|
+
handle text and image turns, downloads resume after a reload, and every export
|
|
6
|
+
is typed.
|
|
11
7
|
|
|
12
8
|
[](https://www.npmjs.com/package/@degreesign/chat)
|
|
13
9
|
[](#licensing)
|
|
@@ -18,77 +14,43 @@ package, then passed to `@degreesign/chat` as text.
|
|
|
18
14
|
|
|
19
15
|
## Table of contents
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
- [
|
|
23
|
-
|
|
24
|
-
- [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- [Status](#status)
|
|
29
|
-
- [Models](#models)
|
|
30
|
-
- [Prompt assembly](#prompt-assembly)
|
|
31
|
-
- [Engines and assets](#engines-and-assets)
|
|
32
|
-
- [Node.js](#nodejs)
|
|
33
|
-
- [React](#react)
|
|
34
|
-
- [Types](#types)
|
|
35
|
-
- [Testing](#testing)
|
|
36
|
-
- [Architecture](#architecture)
|
|
37
|
-
- [FAQ](#faq)
|
|
38
|
-
- [Licensing](#licensing)
|
|
17
|
+
[Why](#why) · [Install](#install) · [Quick start](#quick-start) ·
|
|
18
|
+
[Public API](#public-api) · [Tools](#tools) · [Status](#status) ·
|
|
19
|
+
[Models](#models) · [Prompt assembly](#prompt-assembly) ·
|
|
20
|
+
[Enable WebGPU](#enable-webgpu) · [Engines and assets](#engines-and-assets) ·
|
|
21
|
+
[Node.js](#nodejs) · [React](#react) · [Types](#types) ·
|
|
22
|
+
[Testing](#testing) · [Architecture](#architecture) · [FAQ](#faq) ·
|
|
23
|
+
[Licensing](#licensing)
|
|
39
24
|
|
|
40
25
|
## Why
|
|
41
26
|
|
|
42
|
-
- Proven: [DoListy](https://dolisty.com/#chat), a no-signup PWA,
|
|
27
|
+
- Proven: [DoListy](https://dolisty.com/#chat), a no-signup PWA, describes
|
|
28
|
+
photos, turns voice notes into tasks with `@degreesign/voice`, and answers
|
|
43
29
|
with the network off.
|
|
44
|
-
- On device:
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
- Audio: a user turn can carry an audio URL, sent only to a model with audio
|
|
52
|
-
support. No model in the catalog has an audio encoder.
|
|
53
|
-
- Reusable: the loaded model stays in memory for the next call.
|
|
54
|
-
- Typed: written in TypeScript with declarations for every export.
|
|
55
|
-
- Portable: importing and calling works in browsers, workers, React and Node.
|
|
56
|
-
Local inference stays browser only.
|
|
30
|
+
- On device: weights download once, then every reply is generated locally,
|
|
31
|
+
with no API key and no per-reply cost.
|
|
32
|
+
- Headless and stateless: results arrive through callbacks, no UI is rendered
|
|
33
|
+
and no history is kept; the host passes the whole message list.
|
|
34
|
+
- Two engines behind one runtime: CPU through wllama, WebGPU through LiteRT-LM.
|
|
35
|
+
- Typed and portable: declarations for every export, and importing it works in
|
|
36
|
+
browsers, workers, React and Node.
|
|
57
37
|
|
|
58
38
|
## Install
|
|
59
39
|
|
|
60
40
|
```bash
|
|
61
41
|
npm install @degreesign/chat
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
42
|
yarn add @degreesign/chat
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
43
|
pnpm add @degreesign/chat
|
|
70
44
|
```
|
|
71
45
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
The build emits:
|
|
79
|
-
|
|
80
|
-
- `dist/browser/chat.min.mjs`, the ESM browser entry.
|
|
81
|
-
- `dist/browser/engine-wllama.min.mjs` and
|
|
82
|
-
`dist/browser/engine-litert.min.mjs`, the bundled engines, fetched only when
|
|
83
|
-
a matching model starts.
|
|
84
|
-
- `dist/node/chat.node.min.js` and `chat.node.min.mjs`, the CommonJS and ESM
|
|
85
|
-
Node entries, with the same engine chunks beside them.
|
|
86
|
-
- `dist/assets/wllama/`, the wllama wasm binaries and compat worker.
|
|
87
|
-
- `dist/assets/litert/`, the LiteRT-LM wasm glue and binary.
|
|
88
|
-
- `dist/index.d.ts`, declarations for every export.
|
|
46
|
+
The engines and helpers are bundled into dynamic chunks, so a host resolves no
|
|
47
|
+
engine import and downloads only the chunk a model uses. Only the model weights
|
|
48
|
+
come from outside the package. The build emits `dist/browser/chat.min.mjs` with
|
|
49
|
+
its engine chunks, the CommonJS and ESM Node entries in `dist/node/`, the wasm
|
|
50
|
+
assets in `dist/assets/wllama/` and `dist/assets/litert/`, and `dist/index.d.ts`.
|
|
89
51
|
|
|
90
|
-
Build
|
|
91
|
-
|
|
52
|
+
Build with `yarn build`, typecheck with `yarn typecheck` and test the Node entry
|
|
53
|
+
with `yarn verify:node`.
|
|
92
54
|
|
|
93
55
|
## Quick start
|
|
94
56
|
|
|
@@ -110,10 +72,7 @@ await preloadModel();
|
|
|
110
72
|
const cancel = createChat({
|
|
111
73
|
messages,
|
|
112
74
|
onResponse: (text) => renderReply(text),
|
|
113
|
-
// onThinking
|
|
114
|
-
// onSpeed: (tokensPerSecond) => renderSpeed(tokensPerSecond),
|
|
115
|
-
// onPromptSpeed: (tokensPerSecond) => renderPromptSpeed(tokensPerSecond),
|
|
116
|
-
// onStatus: (snapshot) => renderStatus(snapshot),
|
|
75
|
+
// onThinking, onSpeed, onPromptSpeed and onStatus are optional.
|
|
117
76
|
});
|
|
118
77
|
|
|
119
78
|
// Later, stop the reply, cancel the load, or free the model.
|
|
@@ -123,194 +82,32 @@ await clearModelCache();
|
|
|
123
82
|
```
|
|
124
83
|
|
|
125
84
|
Without `model` the package picks the GPU model when the browser provides a
|
|
126
|
-
WebGPU adapter and the CPU model when it does not.
|
|
127
|
-
`onThinking`, `onSpeed`, `onPromptSpeed` and `onStatus` are optional per call;
|
|
128
|
-
see the API below.
|
|
129
|
-
|
|
130
|
-
There is no promise and no controller object on `createChat`. Every result
|
|
131
|
-
arrives through `onResponse`, and `onStatus` reports `requesting: false` when
|
|
132
|
-
the reply ends. `onSpeed` reports the streamed generation speed in tokens per
|
|
133
|
-
second, measured from the first token so prompt processing is excluded.
|
|
134
|
-
`onPromptSpeed` streams the prompt processing speed while the prompt is
|
|
135
|
-
evaluated, then reports the final value at the first generated token. It uses
|
|
136
|
-
the engine prefill progress or stream timings, and falls back to a running
|
|
137
|
-
estimate when the engine exposes neither.
|
|
138
|
-
`preloadModel` is the one promise: it resolves once the model is loaded, or
|
|
139
|
-
`false` when it cannot load.
|
|
140
|
-
|
|
141
|
-
Attach images to a user turn with `images: [dataUrl]`. The package sends them
|
|
142
|
-
only to a model with vision support, fits each image to 128 to 136 vision
|
|
143
|
-
tokens, snaps both sides to multiples of 32 px, crops the remainder, and caps
|
|
144
|
-
the number with `limits.maxImages`. `qwen3_vl_2b` is the vision model in the
|
|
145
|
-
catalog, and it runs on the CPU engine. A turn sent to a model without vision
|
|
146
|
-
drops its images when the turn also has text, and an image-only turn reports
|
|
147
|
-
`Model does not accept images`.
|
|
148
|
-
|
|
149
|
-
Attach a recording with `audio: [dataUrl]`. No model in the catalog has an
|
|
150
|
-
audio encoder, so a text turn drops the clip and an audio-only turn reports
|
|
151
|
-
`Model does not accept audio`. `limits.maxAudio` still caps the clips
|
|
152
|
-
(default 1).
|
|
153
|
-
|
|
154
|
-
The package never writes storage, so the host appends the user message before
|
|
155
|
-
the call and appends the assistant message after the last `onResponse`.
|
|
156
|
-
|
|
157
|
-
## Case study: DoListy
|
|
158
|
-
|
|
159
|
-
[DoListy](https://dolisty.com/#chat) is a DegreeSign project: a private
|
|
160
|
-
to-do list PWA with no sign-up. It ships `@degreesign/chat` for replies
|
|
161
|
-
and image descriptions, and `@degreesign/voice` for voice notes.
|
|
162
|
-
|
|
163
|
-
I wanted an assistant that could read my daily tasks, but I did not want
|
|
164
|
-
to give 100% of my data to big tech. I was also working on a new project
|
|
165
|
-
that had to stay under wraps until its soft launch, so I could not send
|
|
166
|
-
that work to a hosted model. The assistant had to run on the device, and
|
|
167
|
-
it had to be easy for any developer to drop into an app. Most days I have
|
|
168
|
-
that conversation on my phone: a voice note, a transcript, a few tasks,
|
|
169
|
-
all local. DoListy is where that engine got its first real use.
|
|
170
|
-
|
|
171
|
-
![DoListy assistant answering a question on a phone][dolisty-shot]
|
|
172
|
-
|
|
173
|
-
[dolisty-shot]: https://degreesign.github.io/ds_chat/case-study-dolisty.png
|
|
174
|
-
|
|
175
|
-
*A real session on dolisty.com. The green dot is the local model, not a
|
|
176
|
-
server.*
|
|
177
|
-
|
|
178
|
-
### What it does
|
|
179
|
-
|
|
180
|
-
- Describes a photo and tags it without uploading it.
|
|
181
|
-
- Turns a voice note into a task through `@degreesign/voice`.
|
|
182
|
-
- Writes titles, tags and a running summary in 19 languages. The persona
|
|
183
|
-
is the app's; the package adds no identity text.
|
|
184
|
-
- Answers with the network off, from the model already on disk.
|
|
185
|
-
|
|
186
|
-
### What it costs
|
|
187
|
-
|
|
188
|
-
- No per-reply cost, no API key, no rate limit. The only bill is the
|
|
189
|
-
one-time download: about 1.5 GB for Qwen3-VL, 2.0 GB for Gemma 4.
|
|
190
|
-
- An image waits about 70 seconds for its first token on an 8-thread
|
|
191
|
-
desktop CPU. That is the cost of the CPU vision path, chosen so images
|
|
192
|
-
work on phones without WebGPU; text takes the GPU path when the browser
|
|
193
|
-
has one.
|
|
194
|
-
- A 2 GB download resumes from the last committed part. A reload costs at
|
|
195
|
-
most one part, never the whole file.
|
|
196
|
-
- The gallery describes images in the background while you work.
|
|
197
|
-
|
|
198
|
-
### How it is wired
|
|
199
|
-
|
|
200
|
-
- One `createChat` per send, over a prompt assembled from the app's
|
|
201
|
-
encrypted stores.
|
|
202
|
-
- The reply is committed when `onStatus` reports the end; a newer send
|
|
203
|
-
drops the stale one.
|
|
204
|
-
- `gpuAdapter()` picks Gemma on WebGPU and Qwen everywhere else.
|
|
205
|
-
- One model stays resident, so a switch reloads the engine but never the
|
|
206
|
-
file.
|
|
207
|
-
|
|
208
|
-
**See it work:** open [dolisty.com](https://dolisty.com/#chat) on a phone,
|
|
209
|
-
let the model download once, then turn off the network and ask for a
|
|
210
|
-
summary. Speed numbers are in the
|
|
211
|
-
[web-llm comparison](https://degreesign.github.io/ds_chat/vs_web_llm.html).
|
|
212
|
-
|
|
213
|
-
## Enable WebGPU
|
|
214
|
-
|
|
215
|
-
A request without `model` picks `gemma4_e2b_limited` when the browser exposes a
|
|
216
|
-
WebGPU adapter and `qwen3_vl_2b` when it does not. The Gemma build is the 2.0 GB
|
|
217
|
-
text-only `gemma-4-E2B-it-gpu.litertlm`, and it is the only WebGPU model in the
|
|
218
|
-
catalog, so it loads on a desktop and on a phone alike. Every status snapshot
|
|
219
|
-
reports the build that runs in `model`. Hosts that need images pass
|
|
220
|
-
`qwen3_vl_2b`. The GPU engine is far faster on a desktop, so check the browser
|
|
221
|
-
before blaming the package:
|
|
222
|
-
|
|
223
|
-
```js
|
|
224
|
-
// Run in the console of the page that loads the package
|
|
225
|
-
const adapter = await navigator.gpu?.requestAdapter();
|
|
226
|
-
adapter
|
|
227
|
-
? `${adapter.info.vendor} ${adapter.info.architecture}`
|
|
228
|
-
: `no adapter`;
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
`null` means the browser has the API but no usable adapter, and the package
|
|
232
|
-
falls back to the CPU model. Chromium browsers report the whole state at
|
|
233
|
-
`chrome://gpu`: `WebGPU` should read `Hardware accelerated` and `Vulkan` should
|
|
234
|
-
read `Enabled`. Firefox lists the detected features at `about:support` under
|
|
235
|
-
`Graphics`.
|
|
236
|
-
|
|
237
|
-
### Firefox
|
|
238
|
-
|
|
239
|
-
WebGPU ships enabled on Windows and macOS. On Linux, and on any build that
|
|
240
|
-
reports `navigator.gpu` as undefined:
|
|
241
|
-
|
|
242
|
-
1. Open `about:config`.
|
|
243
|
-
2. Set `dom.webgpu.enabled` to `true`.
|
|
244
|
-
3. Restart the browser and run the snippet again.
|
|
245
|
-
|
|
246
|
-
Firefox runs WebGPU on its own wgpu stack, so an adapter can appear even with a
|
|
247
|
-
software driver. Check the renderer in `about:support` when replies feel slow.
|
|
85
|
+
WebGPU adapter and the CPU model when it does not.
|
|
248
86
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
which is not the default. Start the browser with both flags:
|
|
256
|
-
|
|
257
|
-
```bash
|
|
258
|
-
google-chrome --enable-features=Vulkan --use-angle=vulkan
|
|
259
|
-
microsoft-edge --enable-features=Vulkan --use-angle=vulkan
|
|
260
|
-
brave-browser --enable-features=Vulkan --use-angle=vulkan
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Either flag alone leaves `requestAdapter()` returning `null`. To keep them, add
|
|
264
|
-
them to the `Exec` line of the browser entry under
|
|
265
|
-
`~/.local/share/applications`, or to the target of a desktop shortcut.
|
|
266
|
-
|
|
267
|
-
`--enable-unsafe-webgpu` is a different switch: it only unlocks software
|
|
268
|
-
adapters, so an adapter whose `info.vendor` reads `google` is SwiftShader on the
|
|
269
|
-
CPU, not the GPU. Remove it when measuring speed. Headless Chromium hides
|
|
270
|
-
`navigator.gpu` unless it starts with `--enable-unsafe-webgpu`,
|
|
271
|
-
`--enable-features=Vulkan,WebGPU` and `--use-angle=vulkan`; check the adapter
|
|
272
|
-
there too, because a missing adapter silently selects the CPU model.
|
|
273
|
-
|
|
274
|
-
### GPU memory
|
|
275
|
-
|
|
276
|
-
The WebGPU engine keeps the model and its KV cache in GPU memory. The Gemma
|
|
277
|
-
build carries no vision or audio encoder, so it never starts one and never
|
|
278
|
-
resizes an image. A card that is already busy can lose the device
|
|
279
|
-
(`VK_ERROR_DEVICE_LOST`) during a long generation. If the error appears, close
|
|
280
|
-
other GPU tabs, unload the model, or switch to the CPU model. A lost device
|
|
281
|
-
needs a page or model reload; later calls cannot reuse it.
|
|
282
|
-
|
|
283
|
-
While a reply streams, the GPU engine rests after every token readback, so the
|
|
284
|
-
card stays near 50% of wall time instead of pegging at 100% and starving the
|
|
285
|
-
desktop compositor. `limits.gpuShare` overrides that share for one reply, from
|
|
286
|
-
0.05 to 1; `1` disables the rest and the default is 0.5. Each rest is capped
|
|
287
|
-
at 500 ms, so a very slow token still makes progress. The rest applies after
|
|
288
|
-
the prefill, so the first token is not delayed, but the prefill itself still
|
|
289
|
-
runs unpaced. A freeze during loading or before the first token falls outside
|
|
290
|
-
that rest; use a CPU model on those devices. Unloading releases the WebGPU
|
|
291
|
-
device and resets the LiteRT wasm, because a deleted engine leaves both
|
|
292
|
-
unusable; a later load rebuilds them from the stored model file without
|
|
293
|
-
downloading it again.
|
|
87
|
+
`createChat` returns no promise and no controller. Every result arrives through
|
|
88
|
+
`onResponse`, and `onStatus` reports `requesting: false` when the reply ends.
|
|
89
|
+
`onSpeed` measures from the first token, so it excludes prompt processing. The
|
|
90
|
+
host appends the user and assistant messages itself; the package never writes
|
|
91
|
+
storage. A user turn may carry `images` or `audio`; see
|
|
92
|
+
[Prompt assembly](#prompt-assembly).
|
|
294
93
|
|
|
295
94
|
## Public API
|
|
296
95
|
|
|
297
96
|
```ts
|
|
298
97
|
createChat(input: ChatInput): CancelChat
|
|
299
98
|
preloadModel(input?: ChatPreloadInput): Promise<boolean>
|
|
300
|
-
|
|
301
|
-
|
|
99
|
+
describeImage(input: DescribeImageInput): Promise<string>
|
|
100
|
+
descriptionEnhanced(input: DescriptionEnhancedInput): Promise<DescriptionEnhanced>
|
|
101
|
+
translateTags(input: TranslateTagsInput): Promise<ImageTag[]>
|
|
302
102
|
describeModelId: ChatModelId
|
|
303
103
|
imageReplyTokens: number
|
|
304
104
|
gpuAdapter(): Promise<boolean>
|
|
305
105
|
clearModelCache(): Promise<void>
|
|
306
106
|
```
|
|
307
107
|
|
|
308
|
-
Call it once per response. A new call aborts the previous stream
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
between the CPU describer and the GPU translator. Files stay on disk, and
|
|
312
|
-
`clearModelCache` releases the loaded model and deletes every cached model file
|
|
313
|
-
from this device.
|
|
108
|
+
Call it once per response. A new call aborts the previous stream, and a model
|
|
109
|
+
already loaded skips the download and the load. One model stays resident, and
|
|
110
|
+
`clearModelCache` releases it and deletes every cached file.
|
|
314
111
|
|
|
315
112
|
`ChatInput`:
|
|
316
113
|
|
|
@@ -327,137 +124,143 @@ from this device.
|
|
|
327
124
|
| `onPromptSpeed?` | Receives live prompt processing speed in tokens per second. |
|
|
328
125
|
| `onStatus?` | Receives a `ChatStatusSnapshot` on every state change. |
|
|
329
126
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
tokens (320 on the CPU), 2 images, 1 audio clip, 1000 characters per message,
|
|
335
|
-
10000 prompt characters, 32 CPU threads and a 0.5 GPU busy share. The CPU
|
|
336
|
-
engine ignores `gpuShare`; the GPU engine applies it after prefill.
|
|
127
|
+
The budget defaults are 250 reply tokens, 128 CPU thinking tokens, 1200 GPU
|
|
128
|
+
reasoning tokens (320 on the CPU), 2 images, 1 audio clip, 1000 characters per
|
|
129
|
+
message, 10000 prompt characters, 32 CPU threads and a 0.5 GPU busy share. The
|
|
130
|
+
CPU engine ignores `gpuShare`; the GPU engine applies it after prefill.
|
|
337
131
|
|
|
338
|
-
`CancelChat` is `() => void
|
|
132
|
+
`CancelChat` is `() => void`:
|
|
339
133
|
|
|
340
134
|
| State | Effect |
|
|
341
135
|
| ----- | ------ |
|
|
342
|
-
| Downloading or loading | Stops waiting
|
|
136
|
+
| Downloading or loading | Stops waiting; a shared download keeps going and its bytes stay. |
|
|
343
137
|
| Ready | Aborts the reply and unloads the model that answered. |
|
|
344
138
|
| An older call | Nothing, because a newer call owns the runtime. |
|
|
345
139
|
|
|
346
|
-
The model stays loaded between calls until a cancel frees it, and a newer model
|
|
347
|
-
replaces it. One model download serves every caller: a chat that joins a preload
|
|
348
|
-
shares its progress and never starts a second download.
|
|
349
|
-
|
|
350
140
|
### Preload
|
|
351
141
|
|
|
142
|
+
Call `preloadModel` before the first `createChat` to load early. It resolves
|
|
143
|
+
`true` when the runtime is ready and `false` when the model is unknown,
|
|
144
|
+
unsupported or failed. A preload owns no cancel function, so a cancelled chat
|
|
145
|
+
leaves its download running.
|
|
146
|
+
|
|
147
|
+
`ChatPreloadInput` takes `model?`, `limits?`, `logging?` and `onStatus?`. The
|
|
148
|
+
final snapshot reports `ready: true`, `phase: LlmPhase.load` and `percent: 100`
|
|
149
|
+
on success, or an `error` with `availability: ChatStatus.offline` on failure.
|
|
150
|
+
|
|
151
|
+
### Describe one image
|
|
152
|
+
|
|
352
153
|
```ts
|
|
353
|
-
|
|
154
|
+
describeImage(input: DescribeImageInput): Promise<string>
|
|
354
155
|
```
|
|
355
156
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
157
|
+
Describe one image in English with `qwen3_vl_2b_instruct`, the Q4_K_M build of
|
|
158
|
+
Unsloth Qwen3-VL 2B Instruct. The describer replaces euphemisms with specific
|
|
159
|
+
terms, names visible organs and limbs for living things, and prioritizes
|
|
160
|
+
unusual details. It must never lie, use vague referrals, prefer a
|
|
161
|
+
general term over a precise one, or repeat the request.
|
|
162
|
+
`systemRulesOverride` replaces the default system message. The call uses the
|
|
163
|
+
`imageReplyTokens` budget of 128 tokens unless `limits.maxReplyTokens`
|
|
164
|
+
overrides it, requests 192 vision tokens for its resize, and samples at
|
|
165
|
+
temperature 0.1.
|
|
365
166
|
|
|
366
|
-
`
|
|
167
|
+
`DescribeImageInput` extends `CallOptions`:
|
|
367
168
|
|
|
368
169
|
| Field | Description |
|
|
369
170
|
| ----- | ----------- |
|
|
370
|
-
| `
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
171
|
+
| `image` | One image data URL or URL. |
|
|
172
|
+
| `systemRulesOverride?` | Replaces the default describer system message. |
|
|
173
|
+
| `onSpeed?` | Receives the generation speed, tokens per second. |
|
|
174
|
+
| `onText?` | Receives the accumulated streamed text. |
|
|
374
175
|
|
|
375
|
-
The
|
|
376
|
-
|
|
377
|
-
|
|
176
|
+
The promise resolves the trimmed English description, or an empty string when
|
|
177
|
+
the model returns no text. A finished load reports `percent: 100` and
|
|
178
|
+
`phase: LlmPhase.load` before the first text, so a host can clear its progress
|
|
179
|
+
row.
|
|
378
180
|
|
|
379
|
-
###
|
|
181
|
+
### Enhance a description
|
|
380
182
|
|
|
381
183
|
```ts
|
|
382
|
-
|
|
184
|
+
descriptionEnhanced(
|
|
185
|
+
input: DescriptionEnhancedInput,
|
|
186
|
+
): Promise<DescriptionEnhanced>
|
|
383
187
|
```
|
|
384
188
|
|
|
385
|
-
|
|
386
|
-
build
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
`CANNOT_TRANSLATE`,
|
|
391
|
-
|
|
392
|
-
tokens unless `limits.maxReplyTokens` overrides it. The describer requests
|
|
393
|
-
192 vision tokens for its image resize; the CPU engine keeps its instruct
|
|
394
|
-
window at 16 to 128 tokens, and other models leave the engine image limits
|
|
395
|
-
unset.
|
|
396
|
-
|
|
397
|
-
The describer receives a system message that asks for every element of the
|
|
398
|
-
image and adds rules against lying, verdicts and vague referrals.
|
|
399
|
-
`DescribeImagesInput.additionalRules` appends host rules to that message. The
|
|
400
|
-
translator translates its own previous reply, never the user's instruction,
|
|
401
|
-
preserving meaning, names and numbers without adding or omitting anything.
|
|
402
|
-
|
|
403
|
-
`DescribeImagesInput`:
|
|
189
|
+
Take one English description, translate it when the target is not English, then
|
|
190
|
+
build its gallery tags from the same text. Without a `model` override, the text
|
|
191
|
+
model is `gemma4_e2b_limited` when a WebGPU adapter exists and `qwen3_vl_2b`
|
|
192
|
+
otherwise. The tag pass asks for three English words, then fills the target
|
|
193
|
+
language key of each tag. A failed translation, or the reply
|
|
194
|
+
`CANNOT_TRANSLATE`, keeps the English description. A failed tag call leaves the
|
|
195
|
+
tags empty; a missing tag translation keeps the English tag.
|
|
404
196
|
|
|
405
|
-
|
|
406
|
-
| ----- | ----------- |
|
|
407
|
-
| `targetLanguage` | Language of `description`; `en`, `English` or a tag such as `en-US` skips translation. |
|
|
408
|
-
| `images` | Image data URLs or URLs, described in this order. |
|
|
409
|
-
| `currentIndex?` | Receives `index`, then `index` and `descriptionEN`, then `index`, `descriptionEN`, `description` and the description time in milliseconds. |
|
|
410
|
-
| `limits?` | Partial `LlmLimits` for both the description and translation calls. |
|
|
411
|
-
| `logging?` | Whether package and engine console logs are written. Defaults to `false`. |
|
|
412
|
-
| `onStatus?` | Receives a `ChatStatusSnapshot` while the describer and translator load and run. |
|
|
413
|
-
|
|
414
|
-
The promise resolves one `ImageDescription` per image, in input order:
|
|
197
|
+
`DescriptionEnhancedInput` extends `CallOptions`:
|
|
415
198
|
|
|
416
199
|
| Field | Description |
|
|
417
200
|
| ----- | ----------- |
|
|
418
|
-
| `descriptionEN` | The English description
|
|
419
|
-
| `
|
|
420
|
-
| `
|
|
421
|
-
|
|
422
|
-
`
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
`
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
`
|
|
430
|
-
|
|
431
|
-
`
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
`requesting: false`; when the translator cannot load, it also carries that error
|
|
435
|
-
and the descriptions stay English. An empty `images` list touches no model and
|
|
436
|
-
reports nothing.
|
|
437
|
-
|
|
438
|
-
### Translate descriptions
|
|
201
|
+
| `descriptionEN` | The English description to translate and tag. |
|
|
202
|
+
| `targetLanguage` | Language name used in the prompts, such as `German`. |
|
|
203
|
+
| `targetCode` | Two-letter code used as the tag translation key, such as `de`. |
|
|
204
|
+
| `withTags?` | Whether tags are generated. Defaults to `true`. |
|
|
205
|
+
| `withDescription?` | Whether the description is translated. Defaults to `true`. |
|
|
206
|
+
| `model?` | Text model override; the package picks the GPU build when available. |
|
|
207
|
+
| `onStage?` | Receives `translate`, `tags` or `translateTags` as each pass starts. |
|
|
208
|
+
| `onSpeed?` | Receives the generation speed, tokens per second. |
|
|
209
|
+
| `onText?` | Receives the accumulated streamed text and its stage. |
|
|
210
|
+
|
|
211
|
+
`DescriptionEnhanced` holds `description` (the translation or English
|
|
212
|
+
fallback), `translated` (whether the target language is in use), `tags` (up to
|
|
213
|
+
three, with `en` and the target code, including `nsfw` when flagged) and
|
|
214
|
+
`nsfw?` boolean.
|
|
215
|
+
|
|
216
|
+
### Translate stored tags
|
|
439
217
|
|
|
440
218
|
```ts
|
|
441
|
-
|
|
219
|
+
translateTags(input: TranslateTagsInput): Promise<ImageTag[]>
|
|
442
220
|
```
|
|
443
221
|
|
|
444
|
-
Translate
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
`
|
|
448
|
-
|
|
449
|
-
|
|
222
|
+
Translate tags a gallery already holds, without describing or tagging again.
|
|
223
|
+
Each tag keeps its `en` word and gains the `targetCode` key; a refusal or a
|
|
224
|
+
failed call returns the English tags unchanged. The text model follows the same
|
|
225
|
+
rule as `descriptionEnhanced`, and it is reused when it already sits in memory.
|
|
226
|
+
|
|
227
|
+
`TranslateTagsInput` extends `CallOptions`:
|
|
228
|
+
|
|
229
|
+
| Field | Description |
|
|
230
|
+
| ----- | ----------- |
|
|
231
|
+
| `tags` | The `ImageTag` list to translate. |
|
|
232
|
+
| `targetLanguage` | Language name used in the prompt, such as `German`. |
|
|
233
|
+
| `targetCode` | Two-letter code written as the tag translation key. |
|
|
234
|
+
| `model?` | Text model override; the package picks the GPU build when available. |
|
|
235
|
+
| `onText?` | Receives the accumulated streamed text. |
|
|
236
|
+
|
|
237
|
+
### Call options
|
|
450
238
|
|
|
451
|
-
`
|
|
239
|
+
Every description and tag call takes the same `CallOptions`:
|
|
452
240
|
|
|
453
241
|
| Field | Description |
|
|
454
242
|
| ----- | ----------- |
|
|
455
|
-
| `
|
|
456
|
-
| `
|
|
457
|
-
| `currentIndex?` | Receives `index`, `descriptionEN` and `description` as each entry finishes. |
|
|
458
|
-
| `limits?` | Partial `LlmLimits` for the translation call. |
|
|
243
|
+
| `signal?` | Stops the call and leaves finished work in place. |
|
|
244
|
+
| `limits?` | Partial `LlmLimits` for every model call of the request. |
|
|
459
245
|
| `logging?` | Whether package and engine console logs are written. Defaults to `false`. |
|
|
460
|
-
| `onStatus?` | Receives a `ChatStatusSnapshot` while the
|
|
246
|
+
| `onStatus?` | Receives a `ChatStatusSnapshot` while the model loads and runs. |
|
|
247
|
+
| `onEstimate?` | Receives an `EstimateReport` before the model loads. |
|
|
248
|
+
|
|
249
|
+
### Estimates
|
|
250
|
+
|
|
251
|
+
The three functions report stage durations through `onEstimate`, with `totalEst` and
|
|
252
|
+
a `breakdown` of `operation` and `est` seconds. `describeImage` reports
|
|
253
|
+
`analyze` and `describe`; `descriptionEnhanced` reports `translate`, `tags` and
|
|
254
|
+
`translateTags`; `translateTags` reports `translateTags` alone, leaving out a
|
|
255
|
+
stage that will not run. Measured stages are kept under the `localStorage` key
|
|
256
|
+
`degreesign-chat-estimates-v1`, with a built-in fallback until then.
|
|
257
|
+
|
|
258
|
+
### Deprecated batch helpers
|
|
259
|
+
|
|
260
|
+
`describeImages` and `describeTranslated` remain as wrappers over the two
|
|
261
|
+
single-item functions. They are deprecated, will be removed after November 2027,
|
|
262
|
+
and skip translation when no WebGPU adapter exists. Use `describeImage` and
|
|
263
|
+
`descriptionEnhanced` for new code.
|
|
461
264
|
|
|
462
265
|
## Tools
|
|
463
266
|
|
|
@@ -481,13 +284,10 @@ const answer = await calculation.execute({ expression: `6*7` });
|
|
|
481
284
|
| `WebSearchProvider` | `wikipedia` or `duckduckgo`. |
|
|
482
285
|
|
|
483
286
|
Each tool has `name`, `description`, `parameters` (`ChatToolSchema`) and
|
|
484
|
-
`execute(args)`, which returns a JSON-safe `ChatToolValue` or throws.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
`webSearchTool` takes `WebSearchConfig`: `provider`, a Wikipedia `language`
|
|
489
|
-
edition, `limit` results, `snippetLength` and a `timeout` in milliseconds. The
|
|
490
|
-
search and currency tools fetch over the network; `calculate` does not.
|
|
287
|
+
`execute(args)`, which returns a JSON-safe `ChatToolValue` or throws.
|
|
288
|
+
`webSearchTool` takes `provider`, a Wikipedia `language`, `limit`,
|
|
289
|
+
`snippetLength` and `timeout`. Search and currency fetch over the network;
|
|
290
|
+
`calculate` does not.
|
|
491
291
|
|
|
492
292
|
## Status
|
|
493
293
|
|
|
@@ -509,26 +309,14 @@ search and currency tools fetch over the network; `calculate` does not.
|
|
|
509
309
|
| `error?` | The last failure, cleared when a reply starts. |
|
|
510
310
|
|
|
511
311
|
The first snapshot fires synchronously inside `createChat`. A final snapshot
|
|
512
|
-
with `requesting: false` marks the end
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
waits at least 14 minutes for a slow CPU vision prefill. Once a chunk has
|
|
521
|
-
arrived, 30 seconds of silence ends the stream and keeps the text already
|
|
522
|
-
delivered.
|
|
523
|
-
|
|
524
|
-
## Logging
|
|
525
|
-
|
|
526
|
-
Console output stays off by default. Pass `logging: true` in a call to receive
|
|
527
|
-
the package's censored engine logs, the wllama engine's own logs and the
|
|
528
|
-
LiteRT-LM wasm diagnostics for that call. `describeImages` also logs every stage
|
|
529
|
-
from the model load to the generated English description, including the image
|
|
530
|
-
URL or data URL size and the raw reply. The flag is per call, so a later call
|
|
531
|
-
without it goes quiet again.
|
|
312
|
+
with `requesting: false` marks the end, including a cancelled call.
|
|
313
|
+
`describeImage` and `descriptionEnhanced` report the same snapshots.
|
|
314
|
+
|
|
315
|
+
A stream that never produces its first chunk ends after 5 minutes on a desktop
|
|
316
|
+
or 50 minutes on a phone, and an image turn waits at least 14 minutes for a
|
|
317
|
+
slow CPU prefill. Once a chunk arrives, 30 seconds of silence ends the stream
|
|
318
|
+
and keeps the text already delivered. Console output stays off unless a call
|
|
319
|
+
passes `logging: true`.
|
|
532
320
|
|
|
533
321
|
## Models
|
|
534
322
|
|
|
@@ -539,42 +327,18 @@ without it goes quiet again.
|
|
|
539
327
|
| `qwen3_vl_2b_instruct_q3` | Qwen3-VL 2B Instruct Q3 | No | Yes | No | Yes | No |
|
|
540
328
|
| `gemma4_e2b_limited` | Gemma 4 E2B limited | Yes | No | No | No | Yes |
|
|
541
329
|
|
|
542
|
-
`
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
context on a phone and 8192 on a desktop. Its projector uses the same CPU thread
|
|
555
|
-
budget as the text pass, so image turns wait behind the CPU queue.
|
|
556
|
-
|
|
557
|
-
`qwen3_vl_2b_instruct` is the Unsloth Qwen3-VL 2B Instruct build that
|
|
558
|
-
`describeImages` loads for the English pass, its Q4_K_M file. Both instruct
|
|
559
|
-
builds use Qwen's Q8_0 projector, about 445 MB, so the describer totals about
|
|
560
|
-
1.5 GB on the CPU engine. It has no reasoning pass, so it answers an image
|
|
561
|
-
prompt directly. `qwen3_vl_2b_instruct_q3` is the same model in Q3_K_M, about
|
|
562
|
-
160 MB smaller, for a host that prefers the smaller file and names it
|
|
563
|
-
explicitly.
|
|
564
|
-
|
|
565
|
-
Gemma 4 E2B downloads the 2.0 GB `gemma-4-E2B-it-gpu.litertlm`, the
|
|
566
|
-
precompiled GPU_ARTISAN build Google ships for browsers. It runs on the WebGPU
|
|
567
|
-
engine, carries no vision or audio encoder, and needs no projector download.
|
|
568
|
-
|
|
569
|
-
The WebGPU engine asks for a 32768 token context on a desktop and 16384 on a
|
|
570
|
-
phone, both multiples of 1024. Its int8 KV cache holds the full desktop window
|
|
571
|
-
in about 300 MB, so the larger window stays a small part of the 2 GB model
|
|
572
|
-
footprint.
|
|
573
|
-
|
|
574
|
-
The WebGPU engine keeps one LiteRT conversation for consecutive turns. When the
|
|
575
|
-
next prompt continues that history, only the new user turn is sent. A changed
|
|
576
|
-
history opens a fresh conversation, and older images and audio become `[image]`
|
|
577
|
-
and `[audio]` notes in its preface.
|
|
330
|
+
`qwen3_vl_2b` is the general and vision model: a 445 MB projector with the
|
|
331
|
+
model, about 1.5 GB in total, and a 4096 token context on a phone or 8192 on a
|
|
332
|
+
desktop. Its projector shares the text thread budget, so image turns wait.
|
|
333
|
+
|
|
334
|
+
`qwen3_vl_2b_instruct` is the Unsloth Q4_K_M build that `describeImage` loads,
|
|
335
|
+
with the same Q8_0 projector and no reasoning pass; the Q3_K_M variant is about
|
|
336
|
+
160 MB smaller.
|
|
337
|
+
|
|
338
|
+
`gemma4_e2b_limited` loads the 2.0 GB `gemma-4-E2B-it-gpu.litertlm` with a
|
|
339
|
+
32768 token context on a desktop and 16384 on a phone, without vision or audio
|
|
340
|
+
encoders. A first call downloads it and later calls reuse it; an interrupted
|
|
341
|
+
download resumes from the last committed part.
|
|
578
342
|
|
|
579
343
|
## Prompt assembly
|
|
580
344
|
|
|
@@ -588,135 +352,94 @@ assistant: {next turn}
|
|
|
588
352
|
user: {next turn}
|
|
589
353
|
```
|
|
590
354
|
|
|
591
|
-
- Leading assistant turns are dropped,
|
|
592
|
-
|
|
593
|
-
- Consecutive same-role turns are merged.
|
|
594
|
-
- Messages with no text, images or audio are skipped.
|
|
355
|
+
- Leading assistant turns are dropped, consecutive same-role turns are merged,
|
|
356
|
+
and empty messages are skipped.
|
|
595
357
|
- `maxContentLength` clips each message and `promptCharBudget` bounds the whole
|
|
596
|
-
prompt.
|
|
358
|
+
prompt; image and audio payloads do not count against it.
|
|
597
359
|
- System messages are folded into one leading system turn.
|
|
598
|
-
-
|
|
599
|
-
|
|
600
|
-
-
|
|
601
|
-
|
|
602
|
-
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
360
|
+
- Images and audio come from the newest turns first, capped by `maxImages`
|
|
361
|
+
(default 2) and `maxAudio` (default 1). Older parts are dropped, text stays.
|
|
362
|
+
- Each image keeps its full frame and targets 128 vision tokens, at most 136,
|
|
363
|
+
snapped to multiples of 32 px with the remainder cropped.
|
|
364
|
+
- An image-only turn with a text model reports `Model does not accept images`;
|
|
365
|
+
an audio-only turn reports `Model does not accept audio`.
|
|
366
|
+
|
|
367
|
+
## Enable WebGPU
|
|
368
|
+
|
|
369
|
+
A request without `model` picks `gemma4_e2b_limited` when the browser exposes a
|
|
370
|
+
WebGPU adapter and `qwen3_vl_2b` when it does not. `gpuAdapter()` runs the same
|
|
371
|
+
probe, and every snapshot reports the running build in `model`. Check before
|
|
372
|
+
blaming the package:
|
|
373
|
+
|
|
374
|
+
```js
|
|
375
|
+
const adapter = await navigator.gpu?.requestAdapter();
|
|
376
|
+
adapter
|
|
377
|
+
? `${adapter.info.vendor} ${adapter.info.architecture}`
|
|
378
|
+
: `no adapter`;
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
`null` means the API exists but no adapter does. Chromium reports the state at
|
|
382
|
+
`chrome://gpu`: `WebGPU` should read `Hardware accelerated`. Firefox lists
|
|
383
|
+
features at `about:support`, and on Linux needs `dom.webgpu.enabled` set to
|
|
384
|
+
`true` in `about:config`.
|
|
385
|
+
|
|
386
|
+
Chrome, Edge and Brave ship WebGPU on Windows, macOS and ChromeOS. On Linux,
|
|
387
|
+
start the browser with both flags, since either one alone returns `null`:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
google-chrome --enable-features=Vulkan --use-angle=vulkan
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
`--enable-unsafe-webgpu` only unlocks software adapters: a vendor that reads
|
|
394
|
+
`google` is SwiftShader on the CPU. Headless Chromium needs it plus
|
|
395
|
+
`--enable-features=Vulkan,WebGPU` and `--use-angle=vulkan`.
|
|
396
|
+
|
|
397
|
+
The GPU engine keeps the model and KV cache in GPU memory and rests after every
|
|
398
|
+
token readback, so the card stays near 50% of wall time. `limits.gpuShare`
|
|
399
|
+
overrides that for one reply, from 0.05 to 1; `1` disables the rest, and prefill
|
|
400
|
+
runs unpaced. A busy card can lose the device (`VK_ERROR_DEVICE_LOST`); close
|
|
401
|
+
other GPU tabs or use the CPU model.
|
|
610
402
|
|
|
611
403
|
## Engines and assets
|
|
612
404
|
|
|
613
|
-
The engine is chosen per model:
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
The CPU engine reads `navigator.hardwareConcurrency`, keeps a fifth of the
|
|
627
|
-
logical cores free for the page (at least one) and gives the rest to the
|
|
628
|
-
engine: 8 threads run with 7, 16 with 13, 4 with 3. `limits.maxThreads` only
|
|
629
|
-
lowers the count, and phones follow the same rule; only the context window is
|
|
630
|
-
smaller there. On an 8-thread desktop a 128-token image reaches its first token
|
|
631
|
-
in 71.3 seconds with 7 threads, 82.3 seconds with 6 and 106.6 seconds with 4. A
|
|
632
|
-
JavaScript probe was measured and rejected: it follows JS worker scaling, not
|
|
633
|
-
the ggml pool, and its curve was too noisy to trust.
|
|
634
|
-
|
|
635
|
-
The vision path runs two ggml pools, one for the projector and one for the
|
|
636
|
-
model, so it needs twice the workers of a text model. wllama 3.8.1 pre-spawns
|
|
637
|
-
only `n_threads` workers, which leaves the second pool without workers and
|
|
638
|
-
stalls the encode at 3 or more threads. `scripts/patch-wllama.cjs` sizes the
|
|
639
|
-
pool at twice the thread count on every build until the fix is released
|
|
640
|
-
upstream; with it the image path runs at the heuristic count (7 threads on this
|
|
641
|
-
8-thread desktop) instead of 2. Every CPU model uses one count for the whole
|
|
642
|
-
load.
|
|
643
|
-
|
|
644
|
-
The build bundles wllama from the installed package, so `yarn build` runs
|
|
645
|
-
`scripts/patch-wllama.cjs` first. The script is idempotent and fails loudly when
|
|
646
|
-
the installed wllama no longer matches the patch. `patches/wllama-pool.patch`
|
|
647
|
-
holds the same change as an upstream diff for wllama.
|
|
648
|
-
|
|
649
|
-
The CPU engine needs cross-origin isolation for its threads. Serve the page with
|
|
650
|
-
both headers:
|
|
405
|
+
The engine is chosen per model: WebGPU through LiteRT-LM when the model has a
|
|
406
|
+
`litert` artifact and the browser exposes `navigator.gpu` and `WebAssembly`;
|
|
407
|
+
CPU through wllama when the model has a `cpu` artifact; `LlmStatus.unsupported`
|
|
408
|
+
otherwise, including Node. Both implement `startLLM` and return `model`,
|
|
409
|
+
`backend`, `contextSize`, `chat()` and `unload()`.
|
|
410
|
+
|
|
411
|
+
The CPU engine keeps a fifth of the logical cores free (at least one) and gives
|
|
412
|
+
the rest to the engine, so 8 threads run with 7. `limits.maxThreads` only
|
|
413
|
+
lowers the count. The vision path runs two ggml pools, sized by
|
|
414
|
+
`scripts/patch-wllama.cjs` on every build.
|
|
415
|
+
|
|
416
|
+
The CPU engine needs cross-origin isolation for its threads:
|
|
651
417
|
|
|
652
418
|
```
|
|
653
419
|
Cross-Origin-Opener-Policy: same-origin
|
|
654
420
|
Cross-Origin-Embedder-Policy: require-corp
|
|
655
421
|
```
|
|
656
422
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
Under `require-corp` every cross-origin subresource must be CORS enabled or
|
|
662
|
-
carry `Cross-Origin-Resource-Policy`. Hugging Face downloads already qualify;
|
|
663
|
-
third party images, fonts and embeds need `crossorigin="anonymous"` or must be
|
|
664
|
-
self hosted. Chromium also accepts
|
|
665
|
-
`Cross-Origin-Embedder-Policy: credentialless` when those assets cannot be
|
|
666
|
-
changed. A page inside an iframe is isolated only when every ancestor sets the
|
|
667
|
-
same headers.
|
|
668
|
-
|
|
669
|
-
Each engine keeps its own cache. One model stays loaded, so a switch between
|
|
670
|
-
the CPU and GPU models skips the download but still loads the new engine. The
|
|
671
|
-
previous model's files always stay, so a switch back skips the download.
|
|
672
|
-
`clearModelCache()` deletes them all and frees the storage. Both engines store
|
|
673
|
-
model files in the origin private file system as numbered parts, under
|
|
674
|
-
`wllama-store` for the CPU engine and `litert-store` for the GPU engine. The GPU
|
|
675
|
-
engine also reads a Cache Storage entry under `litertlm-models` when an older
|
|
676
|
-
version stored the file. A quota failure keeps the parts already stored, so
|
|
677
|
-
freeing disk lets the next attempt resume, and the running load streams the
|
|
678
|
-
model from the network so the assistant still starts.
|
|
679
|
-
|
|
680
|
-
Both engines store the files before they load them, and one download serves
|
|
681
|
-
every caller. A failed load, a cancelled call or a repeated `createChat` reads
|
|
682
|
-
the stored parts instead of downloading them again. A download fetches bounded
|
|
683
|
-
ranges of 32 MB, commits each part as it arrives and retries a stalled or cut
|
|
684
|
-
connection from the last committed byte, so a reload loses at most one part.
|
|
685
|
-
Each attempt checks the file size and entity tag, and a changed model starts
|
|
686
|
-
over. A host that ignores the range replays the stored prefix and keeps the
|
|
687
|
-
parts already stored. Hosts without a writable file system still stream from
|
|
688
|
-
the network; the CPU engine falls back to the wllama cache manager and the GPU
|
|
689
|
-
engine to Cache Storage, without resuming.
|
|
690
|
-
|
|
691
|
-
A CPU vision model downloads its projector with the model and stores both files
|
|
692
|
-
as parts. A GPU vision model keeps its encoder in the single `.litertlm` file.
|
|
693
|
-
|
|
694
|
-
Both engines are bundled into `dist/browser` and `dist/node` as dynamic chunks.
|
|
695
|
-
The package ships the wllama binaries under `dist/assets/wllama` and the LiteRT
|
|
696
|
-
wasm under `dist/assets/litert`; model weights remain external.
|
|
423
|
+
Assert `crossOriginIsolated === true` in the page; without isolation the engine
|
|
424
|
+
falls back to one thread. Under `require-corp` every cross-origin subresource
|
|
425
|
+
must be CORS enabled or carry `Cross-Origin-Resource-Policy`.
|
|
697
426
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
downloads and 100% of the load phase while the model compiles.
|
|
427
|
+
Both engines keep resumable parts in the origin private file system, under
|
|
428
|
+
`wllama-store` and `litert-store`. Downloads fetch 32 MB ranges and resume from
|
|
429
|
+
the last committed byte.
|
|
702
430
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
production
|
|
706
|
-
|
|
707
|
-
directory, so copy `dist/assets/litert` beside the bundle as well.
|
|
431
|
+
The package ships the wllama binaries under `dist/assets/wllama` and the LiteRT
|
|
432
|
+
wasm under `dist/assets/litert`. Copy both folders next to the app bundle in
|
|
433
|
+
production; wllama needs its compat worker on the same origin. LiteRT-LM needs
|
|
434
|
+
WebGPU with the JSPI and relaxed SIMD wasm features.
|
|
708
435
|
|
|
709
436
|
## Node.js
|
|
710
437
|
|
|
711
438
|
The package imports and runs without a DOM, so it works in scripts, tests, SSR
|
|
712
|
-
and server side orchestration
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
LiteRT-LM needs WebGPU. In Node, `createChat` reports
|
|
717
|
-
`LlmStatus.unsupported` and `ChatStatus.offline` through `onStatus`, and the
|
|
718
|
-
reply callbacks stay silent. `preloadModel` resolves `false` and reports the
|
|
719
|
-
same unsupported status.
|
|
439
|
+
and server side orchestration with prompt assembly, budgets, status and
|
|
440
|
+
cancellation. Both engines need a browser, so `createChat` there reports
|
|
441
|
+
`LlmStatus.unsupported` and `ChatStatus.offline`, the callbacks stay silent, and
|
|
442
|
+
`preloadModel` resolves `false`.
|
|
720
443
|
|
|
721
444
|
```js
|
|
722
445
|
const { createChat, preloadModel, ChatModelId, LlmRole } = require("@degreesign/chat");
|
|
@@ -726,16 +449,14 @@ const { createChat, preloadModel, ChatModelId, LlmRole } = require("@degreesign/
|
|
|
726
449
|
import { createChat, preloadModel, ChatModelId, LlmRole } from "@degreesign/chat";
|
|
727
450
|
```
|
|
728
451
|
|
|
729
|
-
|
|
730
|
-
`dist/node/chat.node.min.mjs
|
|
731
|
-
through the `node` condition and the browser bundle through the `browser`
|
|
732
|
-
condition.
|
|
452
|
+
`require` resolves `dist/node/chat.node.min.js` and `import` resolves
|
|
453
|
+
`dist/node/chat.node.min.mjs`, both through the exports map.
|
|
733
454
|
|
|
734
455
|
## React
|
|
735
456
|
|
|
736
457
|
Call `createChat` from the send handler, keep the cancel function in a ref, and
|
|
737
|
-
unmount it with the panel. The package never touches the DOM
|
|
738
|
-
|
|
458
|
+
unmount it with the panel. The package never touches the DOM, so the same
|
|
459
|
+
module renders during SSR.
|
|
739
460
|
|
|
740
461
|
```tsx
|
|
741
462
|
import { useEffect, useRef, useState } from "react";
|
|
@@ -751,10 +472,7 @@ const useReply = () => {
|
|
|
751
472
|
setReply("");
|
|
752
473
|
cancelRef.current = createChat({
|
|
753
474
|
model: ChatModelId.qwen3_vl_2b,
|
|
754
|
-
messages: [
|
|
755
|
-
...history,
|
|
756
|
-
{ role: LlmRole.user, content: text },
|
|
757
|
-
],
|
|
475
|
+
messages: [...history, { role: LlmRole.user, content: text }],
|
|
758
476
|
onResponse: setReply,
|
|
759
477
|
});
|
|
760
478
|
};
|
|
@@ -765,123 +483,65 @@ const useReply = () => {
|
|
|
765
483
|
|
|
766
484
|
## Types
|
|
767
485
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
| `ChatStatusSnapshot` | Availability, engine status, backend, progress and request state. |
|
|
778
|
-
| `LlmLimits` | Token, prompt, image, audio, thread and GPU share budgets, all overridable. |
|
|
779
|
-
| `WllamaAssets` | Wasm, compat wasm and compat worker URLs. |
|
|
780
|
-
|
|
781
|
-
The public enums are `ChatModelId`, `LlmRole`, `ChatStatus`, `LlmStatus`,
|
|
782
|
-
`LlmBackend` and `LlmPhase`.
|
|
486
|
+
The inputs are `ChatInput`, `ChatPreloadInput`, `DescribeImageInput`,
|
|
487
|
+
`DescriptionEnhancedInput` and `TranslateTagsInput`, every image and tag input
|
|
488
|
+
built on `CallOptions`; the results are `DescriptionEnhanced`, `EstimateReport`
|
|
489
|
+
and `ImageTag`. `ChatMessage`, `ChatStatusSnapshot`, `LlmLimits`, `CancelChat`
|
|
490
|
+
and `WllamaAssets` describe the rest; the deprecated types are
|
|
491
|
+
`DescribeImagesInput`, `DescribeTranslatedInput` and `ImageDescription`. The
|
|
492
|
+
enums are `ChatModelId`, `LlmRole`, `ChatStatus`, `LlmStatus`, `LlmBackend`,
|
|
493
|
+
`LlmPhase`, `EstimateOperation`, `DescriptionEnhancedStage` and
|
|
494
|
+
`ChatPreloadField`.
|
|
783
495
|
|
|
784
496
|
## Testing
|
|
785
497
|
|
|
786
|
-
The suites live in `test/` and run against the local checkout.
|
|
787
|
-
|
|
788
498
|
`yarn test:instruct` starts a local llama.cpp server with the Qwen3-VL 2B
|
|
789
|
-
Instruct Q4_K_M build:
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
yarn test:instruct
|
|
793
|
-
```
|
|
794
|
-
|
|
795
|
-
It runs `llama serve` on `http://127.0.0.1:8080` with a 160k context, 4
|
|
796
|
-
threads and CPU inference. llama.cpp
|
|
797
|
-
downloads the weights on first run and caches them outside the repository.
|
|
798
|
-
llama.cpp runs about 10x faster than wllama, so prefer the live server
|
|
799
|
-
whenever a test needs real inference. Keep the server in its own terminal
|
|
800
|
-
while the live and image suites run.
|
|
801
|
-
|
|
802
|
-
The image suite bundles the browser harness, serves it from `test/drive.cjs`
|
|
803
|
-
and proxies `/llama/` to the server on port 8080, so start `test:instruct`
|
|
804
|
-
first:
|
|
805
|
-
|
|
806
|
-
```bash
|
|
807
|
-
yarn test:images
|
|
808
|
-
```
|
|
809
|
-
|
|
810
|
-
The fake-engine harness needs no server and never reaches the network.
|
|
499
|
+
Instruct Q4_K_M build on `127.0.0.1:8080`, CPU only, caching the weights outside
|
|
500
|
+
the repository. `yarn test:images` runs the browser suite against it, and the
|
|
501
|
+
fake-engine harness needs no server. `yarn verify:node` checks the Node entry.
|
|
811
502
|
|
|
812
503
|
## Architecture
|
|
813
504
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
A side-by-side comparison with `@mlc-ai/web-llm` for Gemma 4 E2B on WebGPU lives
|
|
820
|
-
at [degreesign.github.io/ds_chat/vs_web_llm.html](https://degreesign.github.io/ds_chat/vs_web_llm.html).
|
|
505
|
+
The [interactive map](https://degreesign.github.io/ds_chat/architecture.html)
|
|
506
|
+
covers the two entries, every source module with line numbers, the helper edges
|
|
507
|
+
and the send flow. A
|
|
508
|
+
[comparison with `@mlc-ai/web-llm`](https://degreesign.github.io/ds_chat/vs_web_llm.html)
|
|
509
|
+
covers Gemma 4 E2B on WebGPU.
|
|
821
510
|
|
|
822
511
|
## FAQ
|
|
823
512
|
|
|
824
513
|
### What is @degreesign/chat?
|
|
825
514
|
|
|
826
515
|
A headless on-device inference controller. It loads one model, streams one reply
|
|
827
|
-
and reports status; the app owns the chat history, storage and UI
|
|
828
|
-
|
|
829
|
-
### Does it store messages?
|
|
830
|
-
|
|
831
|
-
No. The host passes the full prompt on every call and keeps the reply.
|
|
832
|
-
|
|
833
|
-
### Does it support images?
|
|
834
|
-
|
|
835
|
-
Yes, on models with vision support. Put image URLs in a user turn's `images`
|
|
836
|
-
array. Qwen3-VL 2B accepts images on the CPU engine and is the vision model in
|
|
837
|
-
the catalog. Each image keeps its full frame and targets 128 vision tokens, at
|
|
838
|
-
most 136, with both sides snapped to multiples of 32 px and the remainder
|
|
839
|
-
cropped. The WebGPU build is text only, so a model without vision drops images
|
|
840
|
-
when the turn also has text; an image-only turn reports
|
|
841
|
-
`Model does not accept images`.
|
|
842
|
-
|
|
843
|
-
### Does it support audio?
|
|
844
|
-
|
|
845
|
-
No model in the catalog has an audio encoder. A user turn can still carry an
|
|
846
|
-
audio URL in `audio`, and `limits.maxAudio` caps the clips (default 1), but a
|
|
847
|
-
model without audio support drops the clip when the turn also has text, and an
|
|
848
|
-
audio-only turn reports `Model does not accept audio`.
|
|
849
|
-
|
|
850
|
-
### Does it need a server?
|
|
851
|
-
|
|
852
|
-
No. Local chat, model download and inference work without a server. The package
|
|
853
|
-
includes no server component.
|
|
516
|
+
and reports status; the app owns the chat history, storage and UI, passes the
|
|
517
|
+
full prompt on every call and keeps the reply.
|
|
854
518
|
|
|
855
|
-
### Does it
|
|
519
|
+
### Does it support images or audio?
|
|
856
520
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
521
|
+
Images work on models with vision support: put URLs in a user turn's `images`
|
|
522
|
+
array. A model without vision drops them when the turn also has text, and an
|
|
523
|
+
image-only turn reports `Model does not accept images`. No catalog model has an
|
|
524
|
+
audio encoder, so `audio` behaves the same way.
|
|
861
525
|
|
|
862
|
-
###
|
|
526
|
+
### Does it need a server or runtime dependencies?
|
|
863
527
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
engine. The GPU engine also reads a Cache Storage entry under `litertlm-models`
|
|
867
|
-
when an older version stored the file. The package stores no model data itself.
|
|
528
|
+
No. Chat, download and inference run on the device, and the engines ship
|
|
529
|
+
bundled, so a host imports no engine and wires no peer dependency.
|
|
868
530
|
|
|
869
531
|
### Which model should I pick?
|
|
870
532
|
|
|
871
|
-
Qwen3-VL 2B supports reasoning and images on the CPU, and
|
|
872
|
-
with WebAssembly. Gemma 4 E2B
|
|
873
|
-
|
|
874
|
-
broad browser support and for images. Use Gemma on a device with a WebGPU
|
|
875
|
-
adapter when text speed matters.
|
|
533
|
+
Qwen3-VL 2B supports reasoning and images on the CPU, and runs on any browser
|
|
534
|
+
with WebAssembly. Gemma 4 E2B supports reasoning on WebGPU with faster text, but
|
|
535
|
+
a larger download and a slower first compile.
|
|
876
536
|
|
|
877
537
|
## Licensing
|
|
878
538
|
|
|
879
539
|
The runtime code is MIT licensed. The inference engine and model weights are
|
|
880
|
-
distributed by their authors under their own terms
|
|
881
|
-
|
|
540
|
+
distributed by their authors under their own terms, listed in
|
|
541
|
+
`THIRD_PARTIES.md`. See `LICENSE`.
|
|
882
542
|
|
|
883
543
|
## Keywords
|
|
884
544
|
|
|
885
|
-
`@degreesign/chat`, on-device AI chat, local LLM, wllama, litert, webgpu,
|
|
886
|
-
wasm, chat controller, headless chat, TypeScript, browser, speech
|
|
887
|
-
replies.
|
|
545
|
+
`@degreesign/chat`, on-device AI chat, local LLM, wllama, litert, webgpu,
|
|
546
|
+
gemma, wasm, chat controller, headless chat, TypeScript, browser, speech
|
|
547
|
+
friendly replies.
|