@tanstack-ai-browser/webgllm 0.0.0-stage → 0.2.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.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The memory the page's models take, shared by this project's runtimes
3
+ * (Transformers.js, WebGL, wllama), each of which loads models in its own
4
+ * worker. Before a runtime loads a model it claims the memory: other
5
+ * runtimes' least recently used models are unloaded until everything fits
6
+ * the budget. A phone can't hold two 1 GB models (an iPhone Safari tab has
7
+ * 2 GB), and small models still stay loaded side by side.
8
+ *
9
+ * The packages each carry this file and share one registry through
10
+ * globalThis, so they find each other even when bundled apart. Keep the
11
+ * copies identical (packages/{transformers-js,webgl,wllama}/src).
12
+ */
13
+ const KEY = Symbol.for("@tanstack-ai-browser/model-memory");
14
+ function registry() {
15
+ const scope = globalThis;
16
+ return (scope[KEY] ??= { version: 1, held: new Map() });
17
+ }
18
+ /**
19
+ * The default budget: 1.5 GB on iOS (a Safari tab gets 2 GB, the page and
20
+ * Safari take the rest), half the device's memory where the browser says
21
+ * (Chrome: up to 8 GB), else 4 GB.
22
+ */
23
+ export function defaultMemoryBudget() {
24
+ if (typeof navigator === "undefined")
25
+ return Number.POSITIVE_INFINITY;
26
+ const { userAgent, maxTouchPoints } = navigator;
27
+ const ios = /iPhone|iPad|iPod/.test(userAgent) ||
28
+ (/Macintosh/.test(userAgent) && maxTouchPoints > 1);
29
+ if (ios)
30
+ return 1.5e9;
31
+ const gigabytes = navigator.deviceMemory;
32
+ return gigabytes ? gigabytes * 0.5e9 : 4e9;
33
+ }
34
+ /**
35
+ * Sets how many bytes the page's models may take together, across this
36
+ * project's runtimes, from the next load on; undefined for the default.
37
+ */
38
+ export function setMemoryBudget(bytes) {
39
+ registry().budget = bytes;
40
+ }
41
+ /**
42
+ * Makes room for `owner`'s model of about `bytes`, unloading other runtimes'
43
+ * models, least recently used first, until it fits the budget (models kept
44
+ * loaded stay), then records it in place of `owner`'s previous one.
45
+ * Resolves to false, recording nothing, if `signal` aborted meanwhile.
46
+ */
47
+ export async function claimMemory(model) {
48
+ const { signal, ...entry } = model;
49
+ if (signal?.aborted)
50
+ return false;
51
+ const { held, budget } = registry();
52
+ const limit = budget ?? defaultMemoryBudget();
53
+ held.delete(entry.owner);
54
+ const others = [...held.values()].sort((a, b) => a.lastUsed - b.lastUsed);
55
+ let total = others.reduce((sum, h) => sum + h.bytes, entry.bytes);
56
+ for (const other of others) {
57
+ if (total <= limit)
58
+ break;
59
+ if (other.keepLoaded)
60
+ continue;
61
+ held.delete(other.owner);
62
+ total -= other.bytes;
63
+ await other.unload();
64
+ if (signal?.aborted)
65
+ return false;
66
+ }
67
+ held.set(entry.owner, { ...entry, lastUsed: Date.now() });
68
+ return true;
69
+ }
70
+ /** Marks `owner`'s model as just used (for least recently used order). */
71
+ export function touchMemory(owner) {
72
+ const held = registry().held.get(owner);
73
+ if (held)
74
+ held.lastUsed = Date.now();
75
+ }
76
+ /** Forgets `owner`'s model, once it's unloaded or failed to load. */
77
+ export function releaseMemory(owner) {
78
+ registry().held.delete(owner);
79
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The models webgllm runs, from its catalog, with what each supports and
3
+ * its reasoning levels.
4
+ */
5
+ export { WEBGLLM_CHAT_MODELS, WEBGLLM_MODEL_META, WEBGLLM_MODEL_REASONING } from "./catalog.js";
6
+ export type { WebGllmChatModel, WebGllmModelMeta, WebGllmModelReasoningByName } from "./catalog.js";
7
+ //# sourceMappingURL=model-meta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-meta.d.ts","sourceRoot":"","sources":["../src/model-meta.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAC/F,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The models webgllm runs, from its catalog, with what each supports and
3
+ * its reasoning levels.
4
+ */
5
+ export { WEBGLLM_CHAT_MODELS, WEBGLLM_MODEL_META, WEBGLLM_MODEL_REASONING } from "./catalog.js";
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The one `WebGLLM` instance every adapter and helper here shares, and the
3
+ * page-side state around it: which models are ready or downloading, and
4
+ * its place in the memory registry this project's runtimes share
5
+ * (model-memory.ts), so a WebGL model and a Transformers.js or wllama one
6
+ * make room for each other.
7
+ */
8
+ import { WebGLLM } from "webgllm";
9
+ import type { WebGllmChatModel } from "./catalog.js";
10
+ /**
11
+ * Where a model stands in this browser, with the Prompt API's values
12
+ * (`LanguageModel.availability()`): "unavailable" (it can't run here),
13
+ * "downloadable" (it can, once downloaded), "downloading" (a download of it
14
+ * is in progress in this page) or "available" (on the device: no download
15
+ * needed).
16
+ */
17
+ export type Availability = "unavailable" | "downloadable" | "downloading" | "available";
18
+ /** A download's progress, from 0 to 1. */
19
+ export type DownloadProgressCallback = (progress: number) => void;
20
+ /**
21
+ * The shared instance. Each claim its workers make (an estimate before a
22
+ * load, the measured textures after, each growth of the attention cache)
23
+ * goes to the registry first, which makes room across runtimes before the
24
+ * worker allocates. webgllm's own budget holds its models to the same limit
25
+ * and orders them among themselves. webgllm reads `memoryBudget` from its
26
+ * options at each claim, so the getter follows `setMemoryBudget`.
27
+ */
28
+ export declare const webgllm: WebGLLM;
29
+ /**
30
+ * Whether the engine runs in this browser (WebGL 2 with float render
31
+ * targets, in a worker), checked once: webgllm's check makes a WebGL
32
+ * context, and the answer doesn't change during a page's life.
33
+ */
34
+ export declare function engineRuns(): boolean;
35
+ /** Forgets `engineRuns`'s answer (for tests that swap the engine's check). */
36
+ export declare function forgetEngineCheck(): void;
37
+ /**
38
+ * Where `model` stands for chats of up to `contextLength` tokens (as
39
+ * requested: undefined for the model's own), as the Prompt API's
40
+ * `LanguageModel.availability()` says it: "unavailable" where the engine
41
+ * can't run (WebGL 2 with float render targets, in a worker; not on a
42
+ * server) or the model isn't in webgllm's catalog, "downloading" while this
43
+ * page downloads it, "available" when it's loaded for that context or
44
+ * every file it loads is in webgllm's storage (`models.downloaded`, which
45
+ * reads the Origin Private File System only), else "downloadable". No
46
+ * worker, no network.
47
+ */
48
+ export declare function availability(model: WebGllmChatModel, contextLength?: number): Promise<Availability>;
49
+ /**
50
+ * Sets how many bytes the page's models may take together, across this
51
+ * project's runtimes, from the next load on; undefined for the default.
52
+ */
53
+ export declare function setMemoryBudget(bytes: number | undefined): void;
54
+ /**
55
+ * Keeps `model` loaded when another model (of this runtime or of
56
+ * Transformers.js or wllama) loads past the memory budget; by default the
57
+ * least recently used goes. Applies from the model's next load.
58
+ */
59
+ export declare function setKeepLoaded(model: WebGllmChatModel, keep: boolean): void;
60
+ /**
61
+ * Loads a model ahead of time (downloading it the first time) for chats of
62
+ * up to `contextLength` tokens: the model's own context length by default.
63
+ * A different context length loads the model again. Other models stay
64
+ * loaded while they fit the memory budget. `onDownloadProgress` hears the
65
+ * download's progress, 0 to 1, then 1 once the model is ready (at once if
66
+ * it already is).
67
+ */
68
+ export declare function loadModel(model: WebGllmChatModel, options?: {
69
+ contextLength?: number;
70
+ onDownloadProgress?: DownloadProgressCallback;
71
+ /**
72
+ * Stops this caller's wait: it rejects at once with an AbortError and
73
+ * hears no more progress. webgllm shares one load between its callers
74
+ * (a chat's included) and cancels it, leaving the model unloaded, when
75
+ * the last one leaves.
76
+ */
77
+ signal?: AbortSignal;
78
+ }): Promise<void>;
79
+ /**
80
+ * The model loaded for `contextLength` (as requested), loading it if it
81
+ * isn't: what each adapter call waits for before its request.
82
+ */
83
+ export declare function ensureModel(model: WebGllmChatModel, contextLength: number | undefined, signal?: AbortSignal, onDownloadProgress?: DownloadProgressCallback,
84
+ /** Tell `onDownloadProgress` 1 at once if the model is ready already. */
85
+ reportReady?: boolean): Promise<void>;
86
+ /** Marks `model` as just used, for least recently used order across runtimes. */
87
+ export declare function touchModel(model: WebGllmChatModel): void;
88
+ /**
89
+ * Unloads `model`, or every model, freeing its GPU memory; a running
90
+ * generation of it fails. Never rejects (callers often don't wait for it).
91
+ */
92
+ export declare function unloadModel(model?: WebGllmChatModel): Promise<void>;
93
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAMjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAQpD;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GACpB,aAAa,GACb,cAAc,GACd,aAAa,GACb,WAAW,CAAA;AAEf,0CAA0C;AAC1C,MAAM,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;AAUjE;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,SAgBlB,CAAA;AAIF;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAGpC;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,SAEhC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,gBAAgB,EACvB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,YAAY,CAAC,CAYvB;AAKD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,QAExD;AAiDD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,OAAO,QAGnE;AAmCD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,gBAAgB,EACvB,OAAO,GAAE;IACP,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,kBAAkB,CAAC,EAAE,wBAAwB,CAAA;IAC7C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;CAChB,GACL,OAAO,CAAC,IAAI,CAAC,CAaf;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,gBAAgB,EACvB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,MAAM,CAAC,EAAE,WAAW,EACpB,kBAAkB,CAAC,EAAE,wBAAwB;AAC7C,yEAAyE;AACzE,WAAW,UAAQ,GAClB,OAAO,CAAC,IAAI,CAAC,CAef;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,KAAK,EAAE,gBAAgB,QAEjD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnE"}
package/dist/models.js ADDED
@@ -0,0 +1,274 @@
1
+ /**
2
+ * The one `WebGLLM` instance every adapter and helper here shares, and the
3
+ * page-side state around it: which models are ready or downloading, and
4
+ * its place in the memory registry this project's runtimes share
5
+ * (model-memory.ts), so a WebGL model and a Transformers.js or wllama one
6
+ * make room for each other.
7
+ */
8
+ import { WebGLLM } from "webgllm";
9
+ import { defaultMemoryBudget } from "webgllm/advanced";
10
+ import { WEBGLLM_MODEL_META } from "./catalog.js";
11
+ import { claimMemory, releaseMemory, setMemoryBudget as setRegistryBudget, touchMemory } from "./model-memory.js";
12
+ /** The registry's budget (set by any runtime's setMemoryBudget), else the device's. */
13
+ function registryBudget() {
14
+ // Read straight from the shared registry (see model-memory.ts), whose
15
+ // copies in the other packages can set it too.
16
+ const scope = globalThis;
17
+ return scope[Symbol.for("@tanstack-ai-browser/model-memory")]?.budget ?? defaultMemoryBudget();
18
+ }
19
+ /**
20
+ * The shared instance. Each claim its workers make (an estimate before a
21
+ * load, the measured textures after, each growth of the attention cache)
22
+ * goes to the registry first, which makes room across runtimes before the
23
+ * worker allocates. webgllm's own budget holds its models to the same limit
24
+ * and orders them among themselves. webgllm reads `memoryBudget` from its
25
+ * options at each claim, so the getter follows `setMemoryBudget`.
26
+ */
27
+ export const webgllm = new WebGLLM({
28
+ get memoryBudget() {
29
+ return registryBudget();
30
+ },
31
+ onMemoryClaim: async ({ model, bytes, signal }) => {
32
+ await claimMemory({
33
+ owner: owner(model),
34
+ model,
35
+ bytes,
36
+ keepLoaded: keepLoaded.has(model),
37
+ unload: () => evict(model)
38
+ });
39
+ // Unloaded while the registry made room: its "unload" event has already
40
+ // released it, so hold nothing for it.
41
+ if (signal.aborted)
42
+ releaseMemory(owner(model));
43
+ }
44
+ });
45
+ let engineSupported;
46
+ /**
47
+ * Whether the engine runs in this browser (WebGL 2 with float render
48
+ * targets, in a worker), checked once: webgllm's check makes a WebGL
49
+ * context, and the answer doesn't change during a page's life.
50
+ */
51
+ export function engineRuns() {
52
+ engineSupported ??= WebGLLM.isSupported();
53
+ return engineSupported;
54
+ }
55
+ /** Forgets `engineRuns`'s answer (for tests that swap the engine's check). */
56
+ export function forgetEngineCheck() {
57
+ engineSupported = undefined;
58
+ }
59
+ /**
60
+ * Where `model` stands for chats of up to `contextLength` tokens (as
61
+ * requested: undefined for the model's own), as the Prompt API's
62
+ * `LanguageModel.availability()` says it: "unavailable" where the engine
63
+ * can't run (WebGL 2 with float render targets, in a worker; not on a
64
+ * server) or the model isn't in webgllm's catalog, "downloading" while this
65
+ * page downloads it, "available" when it's loaded for that context or
66
+ * every file it loads is in webgllm's storage (`models.downloaded`, which
67
+ * reads the Origin Private File System only), else "downloadable". No
68
+ * worker, no network.
69
+ */
70
+ export async function availability(model, contextLength) {
71
+ if (!(model in WEBGLLM_MODEL_META) || !engineRuns())
72
+ return "unavailable";
73
+ if (downloading.has(model))
74
+ return "downloading";
75
+ if (isLoadedFor(model, contextLength))
76
+ return "available";
77
+ let stored = false;
78
+ try {
79
+ stored = await webgllm.models.downloaded(model);
80
+ }
81
+ catch {
82
+ // Storage that can't be read: a load downloads it.
83
+ }
84
+ if (downloading.has(model))
85
+ return "downloading";
86
+ return stored ? "available" : "downloadable";
87
+ }
88
+ /** Models whose files are downloading in this page. */
89
+ const downloading = new Set();
90
+ /**
91
+ * Sets how many bytes the page's models may take together, across this
92
+ * project's runtimes, from the next load on; undefined for the default.
93
+ */
94
+ export function setMemoryBudget(bytes) {
95
+ setRegistryBudget(bytes);
96
+ }
97
+ /** Models loaded and ready, whatever loaded them. */
98
+ const ready = new Set();
99
+ /**
100
+ * Each loading model's callers' download progress callbacks, until it
101
+ * settles, with how many waiting callers use each: a callback is called
102
+ * once per report, and stays while any caller using it waits.
103
+ */
104
+ const listeners = new Map();
105
+ function call(listener, progress) {
106
+ try {
107
+ listener(progress);
108
+ }
109
+ catch (error) {
110
+ console.error(error);
111
+ }
112
+ }
113
+ /** Calls `model`'s progress callbacks; once it's ready, forgets them. */
114
+ function report(model, progress, settled = false) {
115
+ const waiting = listeners.get(model);
116
+ if (!waiting)
117
+ return;
118
+ if (settled)
119
+ listeners.delete(model);
120
+ for (const listener of [...waiting.keys()])
121
+ call(listener, progress);
122
+ }
123
+ /** Adds a waiting caller's progress callback to `model`'s load. */
124
+ function listen(model, listener) {
125
+ if (!listener)
126
+ return;
127
+ let waiting = listeners.get(model);
128
+ if (!waiting)
129
+ listeners.set(model, (waiting = new Map()));
130
+ waiting.set(listener, (waiting.get(listener) ?? 0) + 1);
131
+ }
132
+ /** Removes one waiting caller's progress callback: other callers using it keep it. */
133
+ function unlisten(model, listener) {
134
+ const waiting = listener && listeners.get(model);
135
+ if (!waiting)
136
+ return;
137
+ const count = (waiting.get(listener) ?? 0) - 1;
138
+ if (count > 0)
139
+ waiting.set(listener, count);
140
+ else
141
+ waiting.delete(listener);
142
+ if (waiting.size === 0)
143
+ listeners.delete(model);
144
+ }
145
+ /** Models never unloaded to make room for another (see setKeepLoaded). */
146
+ const keepLoaded = new Set();
147
+ /**
148
+ * Keeps `model` loaded when another model (of this runtime or of
149
+ * Transformers.js or wllama) loads past the memory budget; by default the
150
+ * least recently used goes. Applies from the model's next load.
151
+ */
152
+ export function setKeepLoaded(model, keep) {
153
+ if (keep)
154
+ keepLoaded.add(model);
155
+ else
156
+ keepLoaded.delete(model);
157
+ }
158
+ /** A model's name in the registry: webgllm holds several models, each its own entry. */
159
+ const owner = (model) => `webgllm:${model}`;
160
+ /** The context each ready model was loaded for, as requested (undefined: the model's own). */
161
+ const loadedFor = new Map();
162
+ /** Whether `model` is ready for `contextLength` (as requested): what a chat needs, and no load. */
163
+ const isLoadedFor = (model, contextLength) => ready.has(model) && loadedFor.has(model) && loadedFor.get(model) === contextLength;
164
+ /** The registry unloads `model` to make room for another runtime's (or another webgllm model). */
165
+ function evict(model) {
166
+ // webgllm's "unload" event then marks it idle and releases its claim.
167
+ return webgllm.unloadModel(model).catch(() => undefined);
168
+ }
169
+ /**
170
+ * Loads `model` for `contextLength`. Its memory is claimed from the worker
171
+ * (see onMemoryClaim) and released at its "unload" event, and its state
172
+ * here follows webgllm's events (see below), not this function. Nothing is awaited
173
+ * before webgllm takes the load, so an unloadModel() from here on reaches it.
174
+ * A caller that gives up releases nothing: webgllm shares one load between
175
+ * its callers and unloads (releasing it) only when the last one leaves.
176
+ */
177
+ async function load(model, contextLength, signal) {
178
+ await webgllm.loadModel(model, {
179
+ ...(contextLength !== undefined && { contextLength }),
180
+ keepLoaded: keepLoaded.has(model),
181
+ ...(signal && { signal })
182
+ });
183
+ loadedFor.set(model, contextLength);
184
+ }
185
+ /**
186
+ * Loads a model ahead of time (downloading it the first time) for chats of
187
+ * up to `contextLength` tokens: the model's own context length by default.
188
+ * A different context length loads the model again. Other models stay
189
+ * loaded while they fit the memory budget. `onDownloadProgress` hears the
190
+ * download's progress, 0 to 1, then 1 once the model is ready (at once if
191
+ * it already is).
192
+ */
193
+ export function loadModel(model, options = {}) {
194
+ const { contextLength, onDownloadProgress, signal } = options;
195
+ const promise = (async () => {
196
+ try {
197
+ await ensureModel(model, contextLength, signal, onDownloadProgress, true);
198
+ }
199
+ catch (error) {
200
+ if (signal?.aborted)
201
+ throw new DOMException("The model load was aborted.", "AbortError");
202
+ throw error;
203
+ }
204
+ })();
205
+ // Callers may leave it: an unawaited failure isn't an unhandled rejection.
206
+ promise.catch(() => undefined);
207
+ return promise;
208
+ }
209
+ /**
210
+ * The model loaded for `contextLength` (as requested), loading it if it
211
+ * isn't: what each adapter call waits for before its request.
212
+ */
213
+ export async function ensureModel(model, contextLength, signal, onDownloadProgress,
214
+ /** Tell `onDownloadProgress` 1 at once if the model is ready already. */
215
+ reportReady = false) {
216
+ signal?.throwIfAborted();
217
+ if (isLoadedFor(model, contextLength)) {
218
+ touchMemory(owner(model));
219
+ if (reportReady && onDownloadProgress)
220
+ call(onDownloadProgress, 1);
221
+ return;
222
+ }
223
+ listen(model, onDownloadProgress);
224
+ try {
225
+ await load(model, contextLength, signal);
226
+ }
227
+ catch (error) {
228
+ // This caller is done waiting; others keep theirs.
229
+ unlisten(model, onDownloadProgress);
230
+ throw error;
231
+ }
232
+ }
233
+ /** Marks `model` as just used, for least recently used order across runtimes. */
234
+ export function touchModel(model) {
235
+ touchMemory(owner(model));
236
+ }
237
+ /**
238
+ * Unloads `model`, or every model, freeing its GPU memory; a running
239
+ * generation of it fails. Never rejects (callers often don't wait for it).
240
+ */
241
+ export function unloadModel(model) {
242
+ return webgllm.unloadModel(model).catch(() => undefined);
243
+ }
244
+ /**
245
+ * A download's share of the way, 0 to 1, from webgllm's progress (which
246
+ * spans downloading, 0 to 0.9, and reading the files into the GPU).
247
+ */
248
+ const downloadShare = ({ progress, loadedBytes, totalBytes }) => totalBytes ? Math.min((loadedBytes ?? 0) / totalBytes, 1) : Math.min(progress / 0.9, 1);
249
+ // webgllm only dispatches events for models in its catalog. The state here
250
+ // follows webgllm's lifecycle events, whatever started the load (loadModel,
251
+ // a completion loading on demand) or the unload (unloadModel, room for
252
+ // another model, a crash).
253
+ webgllm.addEventListener("progress", (progress) => {
254
+ if (progress.phase !== "download" || progress.cached)
255
+ return;
256
+ downloading.add(progress.model);
257
+ report(progress.model, downloadShare(progress));
258
+ });
259
+ webgllm.addEventListener("load", ({ model }) => {
260
+ downloading.delete(model);
261
+ ready.add(model);
262
+ touchMemory(owner(model));
263
+ report(model, 1, true);
264
+ });
265
+ webgllm.addEventListener("error", ({ model }) => {
266
+ downloading.delete(model);
267
+ ready.delete(model);
268
+ });
269
+ webgllm.addEventListener("unload", ({ model }) => {
270
+ downloading.delete(model);
271
+ ready.delete(model);
272
+ loadedFor.delete(model);
273
+ releaseMemory(owner(model));
274
+ });
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@tanstack-ai-browser/webgllm",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "TanStack AI adapter for webgllm: LFM3, LFM2.5 and Qwen3.5 in the browser on WebGL 2, for devices without WebGPU",
5
+ "license": "MIT",
6
+ "author": "daveycodez",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/daveycodez/tanstack-ai-browser.git",
10
+ "directory": "packages/webgllm"
11
+ },
12
+ "homepage": "https://github.com/daveycodez/tanstack-ai-browser/tree/main/packages/webgllm#readme",
13
+ "bugs": "https://github.com/daveycodez/tanstack-ai-browser/issues",
14
+ "keywords": [
15
+ "tanstack-ai",
16
+ "tanstack",
17
+ "ai",
18
+ "llm",
19
+ "webgllm",
20
+ "webgl",
21
+ "webgl2",
22
+ "lfm3",
23
+ "qwen3.5",
24
+ "browser",
25
+ "local-ai"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.js",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ "./package.json": "./package.json",
34
+ ".": {
35
+ "@tanstack-ai-browser/source": "./src/index.ts",
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js",
38
+ "default": "./dist/index.js"
39
+ }
40
+ },
41
+ "files": [
42
+ "src",
43
+ "dist",
44
+ "!**/*.tsbuildinfo"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "nx": {
50
+ "name": "webgllm"
51
+ },
52
+ "peerDependencies": {
53
+ "@tanstack/ai": "^1.0.0"
54
+ },
55
+ "dependencies": {
56
+ "@tanstack/openai-base": "^1.0.4",
57
+ "webgllm": "^0.3.4"
58
+ },
59
+ "devDependencies": {
60
+ "@huggingface/transformers": "4.3.2",
61
+ "@tanstack/ai": "1.4.0",
62
+ "esbuild": "0.28.2"
63
+ },
64
+ "scripts": {
65
+ "test": "node scripts/test.mjs"
66
+ }
6
67
  }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The adapter's runtime methods over webgllm: availability, create with
3
+ * its download progress, destroy, and the catalog's reasoning data.
4
+ */
5
+ import assert from "node:assert/strict"
6
+ import { beforeEach, test } from "node:test"
7
+ import { WebGLLM } from "webgllm"
8
+ import type { LoadProgress } from "webgllm"
9
+
10
+ import { webgllmText } from "./adapter.js"
11
+ import { WEBGLLM_MODEL_REASONING } from "./catalog.js"
12
+ import { fake, MODEL } from "./fake-webgllm.test.js"
13
+
14
+ // Node has no WebGL 2: the engine counts as supported here (unavailable.test.ts has the other case).
15
+ ;(WebGLLM as unknown as { isSupported: () => boolean }).isSupported = () => true
16
+ beforeEach(() => fake.reset())
17
+
18
+ const download = (loadedBytes: number, extra: Partial<LoadProgress> = {}): LoadProgress =>
19
+ ({ model: MODEL, phase: "download", progress: (loadedBytes / 100) * 0.9, loadedBytes, totalBytes: 100, cached: false, ...extra }) as LoadProgress
20
+
21
+ test("availability: downloadable, downloading during a download, available once loaded or stored", async () => {
22
+ const adapter = webgllmText(MODEL)
23
+ assert.equal(await adapter.availability(), "downloadable")
24
+ let release!: () => void
25
+ fake.loadGate = new Promise((resolve) => (release = resolve))
26
+ fake.progress = [download(40)]
27
+ const loading = adapter.create()
28
+ await new Promise((resolve) => setTimeout(resolve, 0))
29
+ assert.equal(await adapter.availability(), "downloading")
30
+ release()
31
+ await loading
32
+ assert.equal(await adapter.availability(), "available")
33
+ await adapter.destroy()
34
+ assert.equal(await adapter.availability(), "downloadable")
35
+ fake.stored.add(MODEL)
36
+ assert.equal(await adapter.availability(), "available")
37
+ })
38
+
39
+ test("create loads for the adapter's context length; progress (the download's share, then 1) reaches both callbacks", async () => {
40
+ const own: Array<number> = []
41
+ const theirs: Array<number> = []
42
+ fake.progress = [download(50), download(50, { cached: true }), download(100)]
43
+ const adapter = webgllmText(MODEL, { contextLength: 2048, onDownloadProgress: (p) => own.push(p) })
44
+ assert.equal(await adapter.create({ onDownloadProgress: (p) => theirs.push(p) }), adapter)
45
+ assert.equal(fake.loads.at(-1)?.options.contextLength, 2048)
46
+ assert.deepEqual(own, [0.5, 1, 1])
47
+ assert.deepEqual(theirs, [0.5, 1, 1])
48
+ })
49
+
50
+ test("create rejects when the load fails or its signal aborts, and is safe to leave unawaited", async () => {
51
+ fake.loadError = new Error("No room for the model.")
52
+ await assert.rejects(webgllmText(MODEL).create(), /No room/)
53
+ fake.loadError = undefined
54
+ fake.loadGate = new Promise(() => undefined)
55
+ const controller = new AbortController()
56
+ const pending = webgllmText(MODEL).create({ signal: controller.signal })
57
+ controller.abort()
58
+ await assert.rejects(pending)
59
+ void webgllmText(MODEL).create({ signal: AbortSignal.abort() })
60
+ })
61
+
62
+ test("destroy unloads the model and never rejects", async () => {
63
+ await webgllmText(MODEL).create()
64
+ await webgllmText(MODEL).destroy()
65
+ assert.deepEqual(fake.unloads, [MODEL])
66
+ assert.equal(fake.loaded.has(MODEL), false)
67
+ })
68
+
69
+ test("WEBGLLM_MODEL_REASONING has every model, in TanStack's shape", () => {
70
+ const reasoning = WEBGLLM_MODEL_REASONING[MODEL]
71
+ assert.ok(reasoning)
72
+ assert.equal(reasoning.levels.high, "high")
73
+ assert.equal(reasoning.levels.off, "none")
74
+ assert.equal(reasoning.budget, true)
75
+ })
76
+
77
+ // Compile-time: each model's reasoning is exact (typecheck fails if it widens again).
78
+ type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false
79
+ const exact = <T extends true>(_: T) => undefined
80
+ type Thinkers = keyof import("./catalog.js").WebGllmModelReasoningByName
81
+ exact<Equal<Thinkers,
82
+ | "LiquidAI/LFM3-1.7B-GGUF"
83
+ | "onnx-community/LFM3-1.7B-ONNX"
84
+ | "LiquidAI/LFM2.5-1.2B-Thinking-ONNX"
85
+ | "LiquidAI/LFM2.5-1.2B-Thinking-GGUF"
86
+ | "LiquidAI/LFM3-VL-2B-GGUF"
87
+ | "LiquidAI/LFM3-VL-2B-ONNX"
88
+ | "unsloth/Qwen3.5-2B-GGUF"
89
+ | "onnx-community/Qwen3.5-2B-ONNX">>(true)
90
+
91
+ test("a worker of your own has a WebGLLM of its own, shared by the adapters given it", async () => {
92
+ const worker = { postMessage: () => undefined, addEventListener: () => undefined, removeEventListener: () => undefined } as unknown as Worker
93
+ type WithEngine = { engine: { webgllm: unknown } }
94
+ const first = webgllmText(MODEL, { worker }) as unknown as WithEngine
95
+ const second = webgllmText(MODEL, { worker }) as unknown as WithEngine
96
+ const page = webgllmText(MODEL) as unknown as WithEngine
97
+ assert.equal(first.engine.webgllm, second.engine.webgllm)
98
+ assert.notEqual(first.engine.webgllm, page.engine.webgllm)
99
+ assert.ok(first.engine.webgllm instanceof WebGLLM)
100
+ })