@degreesign/chat 0.1.3 → 0.3.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @degreesign/chat
2
2
 
3
3
  One-shot on-device AI chat for browsers, workers and Node, running a wllama CPU
4
- engine with a wasm fallback. The host owns the messages and the UI; the package
5
- loads one model, streams one reply and reports status.
4
+ engine or a LiteRT-LM WebGPU engine. The host owns the messages and the UI; the
5
+ package loads one model, streams one reply and reports status.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/@degreesign/chat.svg)](https://www.npmjs.com/package/@degreesign/chat)
8
8
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](#licensing)
@@ -32,8 +32,8 @@ loads one model, streams one reply and reports status.
32
32
  - On device: model weights download once, then every reply is generated locally.
33
33
  - Headless: the package returns text through callbacks and renders no UI.
34
34
  - Stateless: the host passes the whole message list and keeps its own history.
35
- - One engine: CPU through wllama with a wasm fallback, behind one `startLLM`
36
- runtime contract.
35
+ - Two engines: CPU through wllama and WebGPU through LiteRT-LM, behind one
36
+ `startLLM` runtime contract.
37
37
  - Images: a user turn can carry image URLs, sent only to a vision model and
38
38
  capped by the prompt budgets.
39
39
  - Reusable: the loaded model stays in memory for the next call.
@@ -55,20 +55,22 @@ yarn add @degreesign/chat
55
55
  pnpm add @degreesign/chat
56
56
  ```
57
57
 
58
- `@wllama/wllama` and `@wllama/wllama-compat` are build-time dependencies. The
59
- engine is bundled into the published build as a dynamic chunk and its wasm
60
- assets ship beside it, so a consumer installs nothing extra and resolves no
61
- engine import. Only the model weights are downloaded at runtime.
58
+ `@wllama/wllama`, `@wllama/wllama-compat` and `@litert-lm/core` are build-time
59
+ dependencies. The engines are bundled into the published build as dynamic
60
+ chunks and their wasm assets ship beside them, so a consumer installs nothing
61
+ extra and resolves no engine import. Only the model weights are downloaded at
62
+ runtime.
62
63
 
63
64
  The build emits:
64
65
 
65
66
  - `dist/browser/chat.min.mjs`, the ESM browser entry.
66
- - `dist/browser/engine-wllama.min.mjs`, the bundled engine, fetched only when a
67
- model starts.
67
+ - `dist/browser/engine-wllama.min.mjs` and
68
+ `dist/browser/engine-litert.min.mjs`, the bundled engines, fetched only when
69
+ a matching model starts.
68
70
  - `dist/node/chat.node.min.js` and `chat.node.min.mjs`, the CommonJS and ESM
69
- Node entries, with the same engine chunk beside them.
70
- - `dist/assets/wllama/`, the wllama wasm binaries and compat worker served next
71
- to the browser entry.
71
+ Node entries, with the same engine chunks beside them.
72
+ - `dist/assets/wllama/`, the wllama wasm binaries and compat worker.
73
+ - `dist/assets/litert/`, the LiteRT-LM wasm glue and binary.
72
74
  - `dist/index.d.ts`, declarations for every export.
73
75
 
74
76
  Build from source with `yarn build`; typecheck with `yarn typecheck`; test the
@@ -85,6 +87,7 @@ import {
85
87
  ChatModelId,
86
88
  LlmRole,
87
89
  createChat,
90
+ preloadModel,
88
91
  } from "@degreesign/chat";
89
92
 
90
93
  const messages = [
@@ -92,6 +95,9 @@ const messages = [
92
95
  { role: LlmRole.user, content: "Hello" },
93
96
  ];
94
97
 
98
+ // Optional: download and load the model before the first send.
99
+ await preloadModel({ model: ChatModelId.qwen3_vl_2b });
100
+
95
101
  const cancel = createChat({
96
102
  model: ChatModelId.qwen3_vl_2b,
97
103
  messages,
@@ -106,8 +112,10 @@ const cancel = createChat({
106
112
  cancel();
107
113
  ```
108
114
 
109
- There is no promise and no controller object. Every result arrives through the
110
- three callbacks, and `onStatus` reports `requesting: false` when the reply ends.
115
+ There is no promise and no controller object on `createChat`. Every result
116
+ arrives through the three callbacks, and `onStatus` reports `requesting: false`
117
+ when the reply ends. `preloadModel` is the one promise: it resolves once the
118
+ model is loaded, or `false` when it cannot load.
111
119
 
112
120
  Attach images to a user turn with `images: [dataUrl]`. The package sends them
113
121
  only to a model with vision support, center-crops each image to a square,
@@ -120,6 +128,7 @@ the call and appends the assistant message after the last `onResponse`.
120
128
 
121
129
  ```ts
122
130
  createChat(input: ChatInput): CancelChat
131
+ preloadModel(input: ChatPreloadInput): Promise<boolean>
123
132
  ```
124
133
 
125
134
  Call it once per response. A new call aborts the previous stream. When the
@@ -148,6 +157,30 @@ requested model is already loaded, no download or load happens.
148
157
 
149
158
  The model stays loaded between calls until a cancel frees it.
150
159
 
160
+ ### Preload
161
+
162
+ ```ts
163
+ preloadModel(input: ChatPreloadInput): Promise<boolean>
164
+ ```
165
+
166
+ Call it before the first `createChat` to download and load the model early. It
167
+ resolves `true` when the runtime is ready and `false` when the model is unknown,
168
+ unsupported or failed. A later `createChat` for the same model reuses the loaded
169
+ runtime, and either call joins a load already in flight.
170
+
171
+ `ChatPreloadInput` is the subset of `ChatInput` that a preload needs:
172
+
173
+ | Field | Description |
174
+ | ----- | ----------- |
175
+ | `model` | `ChatModelId` to download and load. |
176
+ | `limits?` | Partial `LlmLimits`; `maxThreads` applies when the model loads. |
177
+ | `logging?` | Whether package and engine console logs are written. Defaults to `false`. |
178
+ | `onStatus?` | Receives a `ChatStatusSnapshot` on every progress and state change. |
179
+
180
+ The final snapshot reports `ready: true`, `phase: LlmPhase.load` and
181
+ `percent: 100` on success, or an `error` with `availability: ChatStatus.offline`
182
+ on failure. A preload owns no cancel function; a new load replaces it.
183
+
151
184
  ## Status
152
185
 
153
186
  `ChatStatusSnapshot` is the only lifecycle signal:
@@ -156,7 +189,7 @@ The model stays loaded between calls until a cancel frees it.
156
189
  | ----- | ----------- |
157
190
  | `availability` | `ChatStatus.online`, `.away` or `.offline`, derived from `status`. |
158
191
  | `status` | `LlmStatus`: `idle`, `downloading`, `ready`, `error` or `unsupported`. |
159
- | `backend` | `LlmBackend.cpu` or `.none`. |
192
+ | `backend` | `LlmBackend.cpu`, `.gpu` or `.none`. |
160
193
  | `supportsImages` | Whether the requested model accepts images. |
161
194
  | `ready` | Whether a model is loaded and accepts a reply. |
162
195
  | `loading` | Whether a download or load is in flight. |
@@ -171,20 +204,27 @@ with `requesting: false` marks the end of the call.
171
204
  ## Logging
172
205
 
173
206
  Console output stays off by default. Pass `logging: true` in a call to receive
174
- the package's censored engine logs and the engine's own logs for that call. The
175
- flag is per call, so a later call without it goes quiet again.
207
+ the package's censored engine logs and the wllama engine's own logs for that
208
+ call. The flag is per call, so a later call without it goes quiet again.
209
+ LiteRT-LM writes its wasm diagnostics straight to the console and does not
210
+ follow the flag.
176
211
 
177
212
  ## Models
178
213
 
179
- | `ChatModelId` | Name | Reasoning | Images | CPU |
180
- | ------------- | ---- | --------- | ------ | --- |
181
- | `qwen3_vl_2b` | Qwen3-VL 2B | Yes | Yes | Yes |
214
+ | `ChatModelId` | Name | Reasoning | Images | CPU | WebGPU |
215
+ | ------------- | ---- | --------- | ------ | --- | ------ |
216
+ | `qwen3_vl_2b` | Qwen3-VL 2B | Yes | Yes | Yes | No |
217
+ | `gemma4_e2b` | Gemma 4 E2B | Yes | Yes | No | Yes |
182
218
 
183
- The first call downloads the model through the CPU engine and caches it. Later
219
+ The first call downloads the model through its engine and caches it. Later
184
220
  calls reuse the loaded runtime. Only one model stays in memory.
185
221
 
186
222
  Qwen3-VL 2B downloads a vision projector of about 445 MB with the model, so its
187
- total download is about 1.5 GB.
223
+ total download is about 1.5 GB. It runs on the CPU engine.
224
+
225
+ Gemma 4 E2B downloads one `gemma-4-E2B-it.litertlm` file of about 2.6 GB. It
226
+ runs on the WebGPU engine with its vision encoder in the same file, so an
227
+ image prompt does not download a projector.
188
228
 
189
229
  ## Prompt assembly
190
230
 
@@ -216,22 +256,25 @@ user: {next turn}
216
256
 
217
257
  ## Engines and assets
218
258
 
219
- The engine is chosen per request:
259
+ The engine is chosen per model:
220
260
 
221
- 1. CPU through wllama when a browser exposes `WebAssembly`.
222
- 2. `LlmStatus.unsupported` in Node or in a process without wasm.
261
+ 1. WebGPU through LiteRT-LM when the model has a `litert` artifact and the
262
+ browser exposes `navigator.gpu` and `WebAssembly`.
263
+ 2. CPU through wllama when the model has a `cpu` artifact and the process
264
+ exposes `WebAssembly`.
265
+ 3. `LlmStatus.unsupported` otherwise, including Node.
223
266
 
224
- The CPU engine implements the internal `startLLM({ model, limits, signal,
225
- onProgress })` contract and returns the runtime object with `model`, `backend`,
226
- `contextSize`, `chat()` and `unload()`. `createChat` starts it for the requested
227
- model.
267
+ Both engines implement the internal `startLLM({ model, limits, signal,
268
+ onProgress })` contract and return the runtime object with `model`, `backend`,
269
+ `contextSize`, `chat()` and `unload()`. `createChat` starts the engine that
270
+ matches the requested model.
228
271
 
229
272
  The CPU engine derives a raw thread count from the device, then caps it at
230
273
  `limits.maxThreads`, default 2. The cap is applied after the raw maximum, so the
231
274
  engine never exceeds the device or the caller budget.
232
275
 
233
- Threads only take effect when the host page is cross-origin isolated. Serve it
234
- with both headers:
276
+ The CPU engine needs cross-origin isolation for its threads. Serve the page with
277
+ both headers:
235
278
 
236
279
  ```
237
280
  Cross-Origin-Opener-Policy: same-origin
@@ -250,21 +293,30 @@ self hosted. Chromium also accepts
250
293
  changed. A page inside an iframe is isolated only when every ancestor sets the
251
294
  same headers.
252
295
 
253
- Only one model stays on device. Loading a new model unloads the previous runtime
254
- and deletes other model caches. A quota failure purges every model cache before
255
- retrying, so a half written download never blocks the next attempt.
296
+ Each engine keeps its own cache. Loading a model unloads the previous runtime
297
+ and removes the previous model's cache. The CPU engine uses the wllama cache
298
+ manager; the GPU engine stores the single `.litertlm` file in Cache Storage
299
+ under `litertlm-models`. A quota failure purges the cache of the engine being
300
+ loaded. The GPU cache write is best effort: if the browser rejects it, the model
301
+ still loads from the network stream and the next call downloads it again.
302
+
303
+ A CPU vision model downloads its projector with the model and caches both. A
304
+ GPU vision model keeps its encoder in the single `.litertlm` file.
256
305
 
257
- A vision model downloads its projector with the model and caches both. A cache
258
- written before image support is replaced once, so the projector is fetched.
306
+ Both engines are bundled into `dist/browser` and `dist/node` as dynamic chunks.
307
+ The package ships the wllama binaries under `dist/assets/wllama` and the LiteRT
308
+ wasm under `dist/assets/litert`; model weights remain external.
259
309
 
260
- The engine is bundled into `dist/browser` and `dist/node` as a dynamic chunk.
261
- The package also ships the wllama binaries under `dist/assets/wllama`, and model
262
- weights remain external.
310
+ LiteRT-LM needs WebGPU and the JSPI and relaxed SIMD wasm features, currently
311
+ available in Chromium. It loads a 22.7 MB wasm binary from `dist/assets/litert`
312
+ and the 2.6 GB model file. The GPU engine reports progress while the file
313
+ downloads and 100% of the load phase while the model compiles.
263
314
 
264
315
  wllama needs the compat worker on the same origin. When the app serves the
265
316
  package from `node_modules` in development, auto resolution works. In
266
317
  production, copy `dist/assets/wllama` next to the app bundle so the resolved
267
- asset URLs stay valid.
318
+ asset URLs stay valid. The LiteRT wasm glue finds its binary in the same
319
+ directory, so copy `dist/assets/litert` beside the bundle as well.
268
320
 
269
321
  ## Node.js
270
322
 
@@ -272,16 +324,18 @@ The package imports and runs without a DOM, so it works in scripts, tests, SSR
272
324
  and server side orchestration. Everything except inference is available: prompt
273
325
  assembly, budgets, status state and cancellation.
274
326
 
275
- The wllama engine needs a browser worker and wasm runtime. In Node,
276
- `createChat` reports `LlmStatus.unsupported` and `ChatStatus.offline` through
277
- `onStatus`, and the reply callbacks stay silent.
327
+ Both engines need a browser: wllama needs a worker and a wasm runtime, and
328
+ LiteRT-LM needs WebGPU. In Node, `createChat` reports
329
+ `LlmStatus.unsupported` and `ChatStatus.offline` through `onStatus`, and the
330
+ reply callbacks stay silent. `preloadModel` resolves `false` and reports the
331
+ same unsupported status.
278
332
 
279
333
  ```js
280
- const { createChat, ChatModelId, LlmRole } = require("@degreesign/chat");
334
+ const { createChat, preloadModel, ChatModelId, LlmRole } = require("@degreesign/chat");
281
335
  ```
282
336
 
283
337
  ```js
284
- import { createChat, ChatModelId, LlmRole } from "@degreesign/chat";
338
+ import { createChat, preloadModel, ChatModelId, LlmRole } from "@degreesign/chat";
285
339
  ```
286
340
 
287
341
  The Node entries are `dist/node/chat.node.min.js` for `require` and
@@ -326,6 +380,7 @@ const useReply = () => {
326
380
  | Type | Description |
327
381
  | ---- | ----------- |
328
382
  | `ChatInput` | One request: model, messages, budgets, reasoning and callbacks. |
383
+ | `ChatPreloadInput` | Model, limits, logging and status, the subset `preloadModel` needs. |
329
384
  | `CancelChat` | The function returned by `createChat`. |
330
385
  | `ChatMessage` | One prompt turn, `role`, `content` and optional `images`. |
331
386
  | `ChatStatusSnapshot` | Availability, engine status, backend, progress and request state. |
@@ -356,10 +411,11 @@ No. The host passes the full prompt on every call and keeps the reply.
356
411
  ### Does it support images?
357
412
 
358
413
  Yes, on models with vision support. Put image URLs in a user turn's `images`
359
- array. Qwen3-VL 2B is the shipped model, it accepts images and runs on the CPU
360
- engine. Each image is center-cropped to a square and resized to 288 x 288
361
- before the engine sees it. A model without vision drops images when the turn
362
- also has text; an image-only turn reports `Model does not accept images`.
414
+ array. Qwen3-VL 2B accepts images on the CPU engine and Gemma 4 E2B accepts
415
+ images on the WebGPU engine. Each image is center-cropped to a square and
416
+ resized to 288 x 288 before the engine sees it. A model without vision drops
417
+ images when the turn also has text; an image-only turn reports
418
+ `Model does not accept images`.
363
419
 
364
420
  ### Does it need a server?
365
421
 
@@ -368,20 +424,23 @@ includes no server component.
368
424
 
369
425
  ### Does it have runtime dependencies?
370
426
 
371
- No runtime dependencies. `@wllama/wllama` and `@wllama/wllama-compat` are
372
- build-time dependencies, bundled into the dynamic engine chunk and its wasm
373
- assets, so consumers resolve no engine import and download no engine code beyond
374
- the chunk they use.
427
+ No runtime dependencies. `@wllama/wllama`, `@wllama/wllama-compat` and
428
+ `@litert-lm/core` are build-time dependencies, bundled into the dynamic engine
429
+ chunks and their wasm assets, so consumers resolve no engine import and
430
+ download no engine code beyond the chunk they use.
375
431
 
376
432
  ### Where are the models stored?
377
433
 
378
- The CPU engine owns its cache through the wllama cache manager. The package
379
- stores no model data itself.
434
+ The CPU engine owns its cache through the wllama cache manager. The GPU engine
435
+ stores its model in Cache Storage under `litertlm-models`. The package stores no
436
+ model data itself.
380
437
 
381
438
  ### Which model should I pick?
382
439
 
383
- Qwen3-VL 2B is the only catalog model. It supports reasoning and images and runs
384
- on the CPU engine.
440
+ Qwen3-VL 2B supports reasoning and images on the CPU. Gemma 4 E2B supports
441
+ reasoning and images on WebGPU, with much faster image prompts but a larger
442
+ download and a slower first compile. Use Qwen for broad browser support and
443
+ Gemma when the host has Chromium WebGPU.
385
444
 
386
445
  ## Licensing
387
446
 
@@ -391,5 +450,6 @@ listed in `THIRD_PARTIES.md`. See `LICENSE`.
391
450
 
392
451
  ## Keywords
393
452
 
394
- `@degreesign/chat`, on-device AI chat, local LLM, wllama, wasm, chat controller,
395
- headless chat, TypeScript, browser, speech friendly replies.
453
+ `@degreesign/chat`, on-device AI chat, local LLM, wllama, litert, webgpu, gemma,
454
+ wasm, chat controller, headless chat, TypeScript, browser, speech friendly
455
+ replies.