@degreesign/chat 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DegreeSign
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ The MIT License above covers the runtime code of `@degreesign/chat` only. The
26
+ model weights and the inference engines are licensed separately by their
27
+ authors. See [THIRD_PARTIES.md](THIRD_PARTIES.md) for the attributions and the
28
+ applicable terms.
package/README.md CHANGED
@@ -1,3 +1,375 @@
1
- # Temporary Holding Version
1
+ # @degreesign/chat
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@degreesign/chat.svg)](https://www.npmjs.com/package/@degreesign/chat)
8
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](#licensing)
9
+ [![node](https://img.shields.io/badge/node-%3E%3D24-brightgreen.svg)](#faq)
10
+ [![types](https://img.shields.io/badge/types-included-brightgreen.svg)](#faq)
11
+ [![architecture](https://img.shields.io/badge/architecture-view-blue.svg)](https://degreesign.github.io/ds_chat/)
12
+
13
+ ## Table of contents
14
+
15
+ - [Why](#why)
16
+ - [Install](#install)
17
+ - [Quick start](#quick-start)
18
+ - [Public API](#public-api)
19
+ - [Status](#status)
20
+ - [Models](#models)
21
+ - [Prompt assembly](#prompt-assembly)
22
+ - [Engines and assets](#engines-and-assets)
23
+ - [Node.js](#nodejs)
24
+ - [React](#react)
25
+ - [Types](#types)
26
+ - [Architecture](#architecture)
27
+ - [FAQ](#faq)
28
+ - [Licensing](#licensing)
29
+
30
+ ## Why
31
+
32
+ - On device: model weights download once, then every reply is generated locally.
33
+ - Headless: the package returns text through callbacks and renders no UI.
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.
37
+ - Images: a user turn can carry image URLs, sent only to a vision model and
38
+ capped by the prompt budgets.
39
+ - Reusable: the loaded model stays in memory for the next call.
40
+ - Typed: written in TypeScript with declarations for every export.
41
+ - Portable: importing and calling works in browsers, workers, React and Node.
42
+ Local inference stays browser only.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ npm install @degreesign/chat
48
+ ```
49
+
50
+ ```bash
51
+ yarn add @degreesign/chat
52
+ ```
53
+
54
+ ```bash
55
+ pnpm add @degreesign/chat
56
+ ```
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.
62
+
63
+ The build emits:
64
+
65
+ - `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.
68
+ - `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.
72
+ - `dist/index.d.ts`, declarations for every export.
73
+
74
+ Build from source with `yarn build`; typecheck with `yarn typecheck`; test the
75
+ built Node entry with `yarn verify:node`.
76
+
77
+ ## Quick start
78
+
79
+ Build the full prompt, call `createChat`, and render the accumulated text. The
80
+ call returns one cancel function and starts the model behind the status
81
+ callback.
82
+
83
+ ```ts
84
+ import {
85
+ ChatModelId,
86
+ LlmRole,
87
+ createChat,
88
+ } from "@degreesign/chat";
89
+
90
+ const messages = [
91
+ { role: LlmRole.system, content: "Answer in two sentences." },
92
+ { role: LlmRole.user, content: "Hello" },
93
+ ];
94
+
95
+ const cancel = createChat({
96
+ model: ChatModelId.qwen3_vl_2b,
97
+ messages,
98
+ limits: { maxReplyTokens: 320 },
99
+ reasoning: false,
100
+ onThinking: (text) => renderThinking(text),
101
+ onResponse: (text) => renderReply(text),
102
+ onStatus: (snapshot) => renderStatus(snapshot),
103
+ });
104
+
105
+ // Later, stop the reply, cancel the load, or free the model.
106
+ cancel();
107
+ ```
108
+
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.
111
+
112
+ Attach images to a user turn with `images: [dataUrl]`. The package sends them
113
+ only to a model with vision support, center-crops each image to a square,
114
+ resizes it to 288 x 288, and caps the number with `limits.maxImages`.
115
+
116
+ The package never writes storage, so the host appends the user message before
117
+ the call and appends the assistant message after the last `onResponse`.
118
+
119
+ ## Public API
120
+
121
+ ```ts
122
+ createChat(input: ChatInput): CancelChat
123
+ ```
124
+
125
+ Call it once per response. A new call aborts the previous stream. When the
126
+ requested model is already loaded, no download or load happens.
127
+
128
+ `ChatInput`:
129
+
130
+ | Field | Description |
131
+ | ----- | ----------- |
132
+ | `model` | `ChatModelId` to use for this request. A different model replaces the loaded one. |
133
+ | `messages` | The complete prompt, including system messages and the new user turn. |
134
+ | `limits?` | Partial `LlmLimits`; `maxImages` caps images and `maxThreads` caps CPU threads (default 2). |
135
+ | `reasoning?` | Whether the model may think first. Defaults to `false`. |
136
+ | `logging?` | Whether package and engine console logs are written. Defaults to `false`. |
137
+ | `onThinking?` | Receives the accumulated reasoning text as it streams. |
138
+ | `onResponse` | Receives the accumulated reply text as it streams. Required. |
139
+ | `onStatus?` | Receives a `ChatStatusSnapshot` on every state change. |
140
+
141
+ `CancelChat` is `() => void`. Its meaning depends on the current state:
142
+
143
+ | State | Effect |
144
+ | ----- | ------ |
145
+ | Downloading or loading | Aborts the model load. |
146
+ | Ready | Aborts the reply and unloads the model. |
147
+ | An older call | Nothing, because a newer call owns the runtime. |
148
+
149
+ The model stays loaded between calls until a cancel frees it.
150
+
151
+ ## Status
152
+
153
+ `ChatStatusSnapshot` is the only lifecycle signal:
154
+
155
+ | Field | Description |
156
+ | ----- | ----------- |
157
+ | `availability` | `ChatStatus.online`, `.away` or `.offline`, derived from `status`. |
158
+ | `status` | `LlmStatus`: `idle`, `downloading`, `ready`, `error` or `unsupported`. |
159
+ | `backend` | `LlmBackend.cpu` or `.none`. |
160
+ | `supportsImages` | Whether the requested model accepts images. |
161
+ | `ready` | Whether a model is loaded and accepts a reply. |
162
+ | `loading` | Whether a download or load is in flight. |
163
+ | `requesting` | Whether a reply is streaming right now. |
164
+ | `phase` | `LlmPhase.download` or `.load`. |
165
+ | `percent` | Download or load progress, `0` to `100`. |
166
+ | `error?` | The last failure, cleared when a reply starts. |
167
+
168
+ The first snapshot fires synchronously inside `createChat`. A final snapshot
169
+ with `requesting: false` marks the end of the call.
170
+
171
+ ## Logging
172
+
173
+ 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.
176
+
177
+ ## Models
178
+
179
+ | `ChatModelId` | Name | Reasoning | Images | CPU |
180
+ | ------------- | ---- | --------- | ------ | --- |
181
+ | `qwen3_vl_2b` | Qwen3-VL 2B | Yes | Yes | Yes |
182
+
183
+ The first call downloads the model through the CPU engine and caches it. Later
184
+ calls reuse the loaded runtime. Only one model stays in memory.
185
+
186
+ Qwen3-VL 2B downloads a vision projector of about 445 MB with the model, so its
187
+ total download is about 1.5 GB.
188
+
189
+ ## Prompt assembly
190
+
191
+ The `messages` array is the whole prompt. The package bounds it and shapes it
192
+ for the engine; it adds no identity, language or task text of its own.
193
+
194
+ ```text
195
+ system: {system messages, in list order}
196
+ user: {first user turn}
197
+ assistant: {next turn}
198
+ user: {next turn}
199
+ ```
200
+
201
+ - Leading assistant turns are dropped, because the transcript must start with a
202
+ user turn.
203
+ - Consecutive same-role turns are merged.
204
+ - Messages with no text and no images are skipped.
205
+ - `maxContentLength` clips each message and `promptCharBudget` bounds the whole
206
+ prompt.
207
+ - System messages are folded into one leading system turn.
208
+ - User turns may carry `images`. They are sent only to a model with vision
209
+ support.
210
+ - Images are collected from the newest turns first and capped by `maxImages`
211
+ (default 2). Older image parts are dropped and their text stays.
212
+ - Every image part is center-cropped to a square, then resized to 288 x 288.
213
+ Non-square edges are discarded and smaller images are upscaled.
214
+ - Image payloads do not count against `promptCharBudget`.
215
+ - An image-only turn with a text model reports `Model does not accept images`.
216
+
217
+ ## Engines and assets
218
+
219
+ The engine is chosen per request:
220
+
221
+ 1. CPU through wllama when a browser exposes `WebAssembly`.
222
+ 2. `LlmStatus.unsupported` in Node or in a process without wasm.
223
+
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.
228
+
229
+ The CPU engine derives a raw thread count from the device, then caps it at
230
+ `limits.maxThreads`, default 2. The cap is applied after the raw maximum, so the
231
+ engine never exceeds the device or the caller budget.
232
+
233
+ Only one model stays on device. Loading a new model unloads the previous runtime
234
+ and deletes other model caches. A quota failure purges every model cache before
235
+ retrying, so a half written download never blocks the next attempt.
236
+
237
+ A vision model downloads its projector with the model and caches both. A cache
238
+ written before image support is replaced once, so the projector is fetched.
239
+
240
+ The engine is bundled into `dist/browser` and `dist/node` as a dynamic chunk.
241
+ The package also ships the wllama binaries under `dist/assets/wllama`, and model
242
+ weights remain external.
243
+
244
+ wllama needs the compat worker on the same origin. When the app serves the
245
+ package from `node_modules` in development, auto resolution works. In
246
+ production, copy `dist/assets/wllama` next to the app bundle so the resolved
247
+ asset URLs stay valid.
248
+
249
+ ## Node.js
250
+
251
+ The package imports and runs without a DOM, so it works in scripts, tests, SSR
252
+ and server side orchestration. Everything except inference is available: prompt
253
+ assembly, budgets, status state and cancellation.
254
+
255
+ The wllama engine needs a browser worker and wasm runtime. In Node,
256
+ `createChat` reports `LlmStatus.unsupported` and `ChatStatus.offline` through
257
+ `onStatus`, and the reply callbacks stay silent.
258
+
259
+ ```js
260
+ const { createChat, ChatModelId, LlmRole } = require("@degreesign/chat");
261
+ ```
262
+
263
+ ```js
264
+ import { createChat, ChatModelId, LlmRole } from "@degreesign/chat";
265
+ ```
266
+
267
+ The Node entries are `dist/node/chat.node.min.js` for `require` and
268
+ `dist/node/chat.node.min.mjs` for `import`. The package exports map selects them
269
+ through the `node` condition and the browser bundle through the `browser`
270
+ condition.
271
+
272
+ ## React
273
+
274
+ Call `createChat` from the send handler, keep the cancel function in a ref, and
275
+ unmount it with the panel. The package never touches the DOM while it is
276
+ imported or called, so the same module renders during SSR.
277
+
278
+ ```tsx
279
+ import { useEffect, useRef, useState } from "react";
280
+ import { ChatModelId, LlmRole, createChat } from "@degreesign/chat";
281
+
282
+ const useReply = () => {
283
+ const cancelRef = useRef<(() => void) | null>(null);
284
+ const [reply, setReply] = useState("");
285
+
286
+ useEffect(() => () => cancelRef.current?.(), []);
287
+
288
+ const send = (history, text) => {
289
+ setReply("");
290
+ cancelRef.current = createChat({
291
+ model: ChatModelId.qwen3_vl_2b,
292
+ messages: [
293
+ ...history,
294
+ { role: LlmRole.user, content: text },
295
+ ],
296
+ onResponse: setReply,
297
+ });
298
+ };
299
+
300
+ return { reply, send };
301
+ };
302
+ ```
303
+
304
+ ## Types
305
+
306
+ | Type | Description |
307
+ | ---- | ----------- |
308
+ | `ChatInput` | One request: model, messages, budgets, reasoning and callbacks. |
309
+ | `CancelChat` | The function returned by `createChat`. |
310
+ | `ChatMessage` | One prompt turn, `role`, `content` and optional `images`. |
311
+ | `ChatStatusSnapshot` | Availability, engine status, backend, progress and request state. |
312
+ | `LlmLimits` | Token, prompt, image and thread budgets, all overridable. |
313
+ | `WllamaAssets` | Wasm, compat wasm and compat worker URLs. |
314
+
315
+ The public enums are `ChatModelId`, `LlmRole`, `ChatStatus`, `LlmStatus`,
316
+ `LlmBackend` and `LlmPhase`.
317
+
318
+ ## Architecture
319
+
320
+ An interactive map of the package lives at
321
+ [degreesign.github.io/ds_chat](https://degreesign.github.io/ds_chat/). It covers
322
+ the two entries, every source module with line numbers, the helper edges and the
323
+ send flow.
324
+
325
+ ## FAQ
326
+
327
+ ### What is @degreesign/chat?
328
+
329
+ A headless on-device inference controller. It loads one model, streams one reply
330
+ and reports status; the app owns the chat history, storage and UI.
331
+
332
+ ### Does it store messages?
333
+
334
+ No. The host passes the full prompt on every call and keeps the reply.
335
+
336
+ ### Does it support images?
337
+
338
+ Yes, on models with vision support. Put image URLs in a user turn's `images`
339
+ array. Qwen3-VL 2B is the shipped model, it accepts images and runs on the CPU
340
+ engine. Each image is center-cropped to a square and resized to 288 x 288
341
+ before the engine sees it. A model without vision drops images when the turn
342
+ also has text; an image-only turn reports `Model does not accept images`.
343
+
344
+ ### Does it need a server?
345
+
346
+ No. Local chat, model download and inference work without a server. The package
347
+ includes no server component.
348
+
349
+ ### Does it have runtime dependencies?
350
+
351
+ No runtime dependencies. `@wllama/wllama` and `@wllama/wllama-compat` are
352
+ build-time dependencies, bundled into the dynamic engine chunk and its wasm
353
+ assets, so consumers resolve no engine import and download no engine code beyond
354
+ the chunk they use.
355
+
356
+ ### Where are the models stored?
357
+
358
+ The CPU engine owns its cache through the wllama cache manager. The package
359
+ stores no model data itself.
360
+
361
+ ### Which model should I pick?
362
+
363
+ Qwen3-VL 2B is the only catalog model. It supports reasoning and images and runs
364
+ on the CPU engine.
365
+
366
+ ## Licensing
367
+
368
+ The runtime code is MIT licensed. The inference engine and model weights are
369
+ distributed by their authors under their own terms. Their items and terms are
370
+ listed in `THIRD_PARTIES.md`. See `LICENSE`.
371
+
372
+ ## Keywords
373
+
374
+ `@degreesign/chat`, on-device AI chat, local LLM, wllama, wasm, chat controller,
375
+ headless chat, TypeScript, browser, speech friendly replies.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Xuan Son NGUYEN
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.