@happyvertical/smrt-svelte 0.51.11 → 0.51.13

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.
Files changed (27) hide show
  1. package/AGENTS.md +27 -0
  2. package/dist/browser-ai/adapters/llm/bitgpu-inference-backend.d.ts +129 -0
  3. package/dist/browser-ai/adapters/llm/bitgpu-inference-backend.d.ts.map +1 -0
  4. package/dist/browser-ai/adapters/llm/bitgpu-inference-backend.js +331 -0
  5. package/dist/browser-ai/adapters/llm/bitgpu-inference-backend.test.js +464 -0
  6. package/dist/browser-ai/adapters/llm/index.d.ts +2 -0
  7. package/dist/browser-ai/adapters/llm/index.d.ts.map +1 -1
  8. package/dist/browser-ai/adapters/llm/index.js +2 -0
  9. package/dist/browser-ai/adapters/llm/types.d.ts +18 -0
  10. package/dist/browser-ai/adapters/llm/types.d.ts.map +1 -1
  11. package/dist/browser-ai/adapters/llm/webllm-inference-backend.d.ts +67 -0
  12. package/dist/browser-ai/adapters/llm/webllm-inference-backend.d.ts.map +1 -0
  13. package/dist/browser-ai/adapters/llm/webllm-inference-backend.js +364 -0
  14. package/dist/browser-ai/adapters/llm/webllm-inference-backend.test.js +646 -0
  15. package/dist/browser-ai/adapters/llm/webllm.d.ts.map +1 -1
  16. package/dist/browser-ai/adapters/llm/webllm.js +19 -2
  17. package/dist/browser-ai/svelte/components/ModelStatusControl.svelte +190 -0
  18. package/dist/browser-ai/svelte/components/ModelStatusControl.svelte.d.ts +17 -0
  19. package/dist/browser-ai/svelte/components/ModelStatusControl.svelte.d.ts.map +1 -0
  20. package/dist/browser-ai/svelte/components/__tests__/ModelStatusControl.test.js +172 -0
  21. package/dist/browser-ai/svelte/index.d.ts +2 -1
  22. package/dist/browser-ai/svelte/index.d.ts.map +1 -1
  23. package/dist/browser-ai/svelte/index.js +3 -1
  24. package/dist/i18n/strings.browser-ai.d.ts +4 -0
  25. package/dist/i18n/strings.browser-ai.d.ts.map +1 -0
  26. package/dist/i18n/strings.browser-ai.js +4 -0
  27. package/package.json +19 -15
@@ -0,0 +1,464 @@
1
+ /**
2
+ * The `bitgpu` inference backend.
3
+ *
4
+ * Beyond the sibling WebLLM suite's invariants, these three are what make this
5
+ * backend worth having:
6
+ *
7
+ * 1. **`signal` is forwarded.** bitgpu is the only backend in this package that
8
+ * can abort a live generation, so losing the option would make the one
9
+ * cancellation-capable path behave like the ones that cannot cancel.
10
+ * 2. **A turn before `load()` fails with a named reason.** These weights run from
11
+ * hundreds of MB to multiple GB; an implicit download behind the first turn is
12
+ * the failure this refuses to have.
13
+ * 3. **Progress maps onto the mirrored shape**, so `DownloadProgress` renders it
14
+ * without acquiring a second vocabulary.
15
+ */
16
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
17
+ import { createBitGpuInferenceBackend, } from './bitgpu-inference-backend.js';
18
+ const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
19
+ /** jsdom has no `navigator.gpu`; detectCapabilities keys off `'gpu' in navigator`. */
20
+ function withWebGpu() {
21
+ Object.defineProperty(globalThis, 'navigator', {
22
+ value: { ...globalThis.navigator, gpu: {} },
23
+ configurable: true,
24
+ });
25
+ }
26
+ function makeModules() {
27
+ let onProgress;
28
+ let engineFailure = null;
29
+ let markEngineReady = () => { };
30
+ const whenEngineReady = new Promise((resolve) => {
31
+ markEngineReady = resolve;
32
+ });
33
+ const d = {
34
+ engineCalls: 0,
35
+ chatCalls: 0,
36
+ sends: [],
37
+ streams: [],
38
+ disposed: 0,
39
+ emitProgress: (report) => onProgress?.(report),
40
+ failEngine: (error) => {
41
+ engineFailure = error;
42
+ },
43
+ streamDeltas: ['Hel', 'lo'],
44
+ finishReason: 'stop',
45
+ whenEngineReady,
46
+ };
47
+ const engine = {
48
+ dispose() {
49
+ d.disposed += 1;
50
+ },
51
+ };
52
+ // Typed as the interfaces the backend declares, so the compiler checks the
53
+ // double rather than a cast asserting it.
54
+ const bitgpu = {
55
+ async createEngine(options) {
56
+ d.engineCalls += 1;
57
+ if (engineFailure)
58
+ throw engineFailure;
59
+ onProgress = typeof options === 'string' ? undefined : options.onProgress;
60
+ markEngineReady();
61
+ return engine;
62
+ },
63
+ WebGPUUnavailableError: class extends Error {
64
+ },
65
+ };
66
+ const chatModule = {
67
+ async createChat() {
68
+ d.chatCalls += 1;
69
+ return {
70
+ async send(messages, options) {
71
+ d.sends.push({ messages, options });
72
+ return { text: 'reply', finishReason: d.finishReason };
73
+ },
74
+ async *stream(messages, options) {
75
+ d.streams.push({ messages, options });
76
+ for (const delta of d.streamDeltas)
77
+ yield delta;
78
+ return {
79
+ text: d.streamDeltas.join(''),
80
+ finishReason: d.finishReason,
81
+ };
82
+ },
83
+ };
84
+ },
85
+ };
86
+ return {
87
+ options: {
88
+ loadBitGpu: async () => bitgpu,
89
+ loadChat: async () => chatModule,
90
+ },
91
+ d,
92
+ };
93
+ }
94
+ async function expectPathError(run, code) {
95
+ const error = await run().then(() => undefined, (thrown) => thrown);
96
+ expect(error).toMatchObject({ name: 'InferencePathError', code });
97
+ }
98
+ const USER = [{ role: 'user', content: 'hi' }];
99
+ describe('createBitGpuInferenceBackend', () => {
100
+ beforeEach(() => {
101
+ Object.defineProperty(globalThis, 'navigator', {
102
+ value: { ...globalThis.navigator },
103
+ configurable: true,
104
+ });
105
+ });
106
+ afterEach(() => {
107
+ if (originalNavigator) {
108
+ Object.defineProperty(globalThis, 'navigator', originalNavigator);
109
+ }
110
+ });
111
+ it('reports unavailable without WebGPU', () => {
112
+ const { options } = makeModules();
113
+ const backend = createBitGpuInferenceBackend(options);
114
+ expect(backend.status).toBe('unavailable');
115
+ expect(backend.kind).toBe('local');
116
+ });
117
+ it('refuses to load without WebGPU rather than starting a doomed download', async () => {
118
+ const { options, d } = makeModules();
119
+ const backend = createBitGpuInferenceBackend(options);
120
+ await expectPathError(() => backend.load(), 'no_usable_backend');
121
+ expect(d.engineCalls).toBe(0);
122
+ });
123
+ it('reports idle before load and ready after', async () => {
124
+ withWebGpu();
125
+ const { options } = makeModules();
126
+ const backend = createBitGpuInferenceBackend(options);
127
+ expect(backend.status).toBe('idle');
128
+ await backend.load();
129
+ expect(backend.status).toBe('ready');
130
+ });
131
+ it('reports loading before the first await resolves', () => {
132
+ withWebGpu();
133
+ const { options } = makeModules();
134
+ const backend = createBitGpuInferenceBackend(options);
135
+ // `idle` is the state auto-selection is told to skip, so the window between
136
+ // clicking Load and the engine resolving must not report it.
137
+ void backend.load();
138
+ expect(backend.status).toBe('loading');
139
+ });
140
+ it('maps a bytes report onto the mirrored progress shape', async () => {
141
+ withWebGpu();
142
+ const { options, d } = makeModules();
143
+ const backend = createBitGpuInferenceBackend(options);
144
+ const seen = [];
145
+ const load = backend.load((p) => seen.push(p));
146
+ await d.whenEngineReady;
147
+ d.emitProgress({ phase: 'weights', loaded: 50, total: 200 });
148
+ expect(backend.progress).toMatchObject({
149
+ state: 'downloading',
150
+ bytesLoaded: 50,
151
+ bytesTotal: 200,
152
+ percent: 25,
153
+ currentFile: 'weights',
154
+ });
155
+ await load;
156
+ expect(seen[0]).toMatchObject({ percent: 25 });
157
+ // Cleared once the model is usable.
158
+ expect(backend.progress).toBeUndefined();
159
+ });
160
+ it('reports the pipeline phase as unpacking, without inventing a percent', async () => {
161
+ withWebGpu();
162
+ const { options, d } = makeModules();
163
+ const backend = createBitGpuInferenceBackend(options);
164
+ const load = backend.load();
165
+ await d.whenEngineReady;
166
+ d.emitProgress({ phase: 'pipelines' });
167
+ expect(backend.progress).toMatchObject({ state: 'extracting', percent: 0 });
168
+ await load;
169
+ });
170
+ it('reports error status and the failure when a load rejects', async () => {
171
+ withWebGpu();
172
+ const { options, d } = makeModules();
173
+ d.failEngine(new Error('out of GPU memory'));
174
+ const backend = createBitGpuInferenceBackend(options);
175
+ await expect(backend.load()).rejects.toThrow('out of GPU memory');
176
+ expect(backend.status).toBe('error');
177
+ expect(backend.progress).toMatchObject({
178
+ state: 'error',
179
+ error: 'out of GPU memory',
180
+ });
181
+ });
182
+ it('re-enters on a retry after a failed load', async () => {
183
+ withWebGpu();
184
+ const { options, d } = makeModules();
185
+ d.failEngine(new Error('transient'));
186
+ const backend = createBitGpuInferenceBackend(options);
187
+ await expect(backend.load()).rejects.toThrow('transient');
188
+ d.failEngine(null);
189
+ await backend.load();
190
+ // The second attempt must start a new engine, not replay the rejection.
191
+ expect(d.engineCalls).toBe(2);
192
+ expect(backend.status).toBe('ready');
193
+ });
194
+ it('shares one load between concurrent callers', async () => {
195
+ withWebGpu();
196
+ const { options, d } = makeModules();
197
+ const backend = createBitGpuInferenceBackend(options);
198
+ await Promise.all([backend.load(), backend.load(), backend.load()]);
199
+ expect(d.engineCalls).toBe(1);
200
+ });
201
+ it('is a no-op when the engine is already ready', async () => {
202
+ withWebGpu();
203
+ const { options, d } = makeModules();
204
+ const backend = createBitGpuInferenceBackend(options);
205
+ await backend.load();
206
+ expect(d.engineCalls).toBe(1);
207
+ // `load` is documented as a no-op when already ready. A second call that
208
+ // re-entered would fetch the weights again and orphan the live engine,
209
+ // since nothing disposes the reference it replaces.
210
+ await backend.load();
211
+ expect(d.engineCalls).toBe(1);
212
+ expect(d.chatCalls).toBe(1);
213
+ expect(d.disposed).toBe(0);
214
+ expect(backend.status).toBe('ready');
215
+ });
216
+ it('retries after a loader that throws synchronously', async () => {
217
+ withWebGpu();
218
+ const { options } = makeModules();
219
+ let failing = true;
220
+ const backend = createBitGpuInferenceBackend({
221
+ ...options,
222
+ loadBitGpu: () => {
223
+ // Synchronous, not a rejected promise: legal for the declared
224
+ // `() => Promise<BitGpuModule>` and what an absent module shim does.
225
+ if (failing)
226
+ throw new Error('bitgpu is not installed');
227
+ return options.loadBitGpu();
228
+ },
229
+ });
230
+ await expect(backend.load()).rejects.toThrow('bitgpu is not installed');
231
+ expect(backend.status).toBe('error');
232
+ failing = false;
233
+ await backend.load();
234
+ // A synchronous throw settles the load body before it is stored, so a
235
+ // cached rejection here would replay on every later attempt.
236
+ expect(backend.status).toBe('ready');
237
+ });
238
+ it('discards a load that overlaps an unload', async () => {
239
+ withWebGpu();
240
+ const { options, d } = makeModules();
241
+ const backend = createBitGpuInferenceBackend(options);
242
+ const pending = backend.load();
243
+ await backend.unload();
244
+ await pending;
245
+ // Publishing the engine here would report `ready` with a live GPU device
246
+ // nothing disposes, and a later `load()` would no-op on it.
247
+ expect(backend.status).toBe('idle');
248
+ expect(d.disposed).toBe(1);
249
+ });
250
+ it('stays idle when a load rejects after an unload', async () => {
251
+ withWebGpu();
252
+ const { options, d } = makeModules();
253
+ d.failEngine(new Error('out of GPU memory'));
254
+ const backend = createBitGpuInferenceBackend(options);
255
+ const pending = backend.load();
256
+ await backend.unload();
257
+ await expect(pending).rejects.toThrow('out of GPU memory');
258
+ // The caller cancelled the work, so the backend must not report an error
259
+ // for it — and `unload()` already cleared any prior failure.
260
+ expect(backend.status).toBe('idle');
261
+ expect(backend.progress).toBeUndefined();
262
+ });
263
+ it('starts real work when load() follows an unload', async () => {
264
+ withWebGpu();
265
+ const { options, d } = makeModules();
266
+ const backend = createBitGpuInferenceBackend(options);
267
+ const discarded = backend.load();
268
+ await backend.unload();
269
+ // Issued while the discarded attempt is still pending: handing back that
270
+ // attempt's promise would resolve this call without loading anything.
271
+ const second = backend.load();
272
+ await Promise.all([discarded, second]);
273
+ expect(d.engineCalls).toBe(2);
274
+ expect(backend.status).toBe('ready');
275
+ });
276
+ it('disposes the engine it created when createChat fails', async () => {
277
+ withWebGpu();
278
+ const { options, d } = makeModules();
279
+ const backend = createBitGpuInferenceBackend({
280
+ ...options,
281
+ // The tokenizer comes from a different host than the manifest, so a
282
+ // failing `createChat` is the documented real failure after the engine
283
+ // already exists.
284
+ loadChat: async () => ({
285
+ async createChat() {
286
+ throw new Error('tokenizer 404');
287
+ },
288
+ }),
289
+ });
290
+ await expect(backend.load()).rejects.toThrow('tokenizer 404');
291
+ // Nothing else holds the engine, so the abandoned attempt must release it —
292
+ // otherwise every retry allocates another GPU device over the leaked one.
293
+ expect(d.disposed).toBe(1);
294
+ expect(backend.status).toBe('error');
295
+ });
296
+ it('keeps the load failure when the teardown throws', async () => {
297
+ withWebGpu();
298
+ const { options } = makeModules();
299
+ const backend = createBitGpuInferenceBackend({
300
+ ...options,
301
+ loadBitGpu: async () => ({
302
+ async createEngine() {
303
+ return {
304
+ dispose() {
305
+ throw new Error('device lost');
306
+ },
307
+ };
308
+ },
309
+ WebGPUUnavailableError: class extends Error {
310
+ },
311
+ }),
312
+ loadChat: async () => ({
313
+ async createChat() {
314
+ throw new Error('tokenizer 404');
315
+ },
316
+ }),
317
+ });
318
+ // The teardown runs while abandoning the attempt; its own error must not
319
+ // replace the failure the caller is owed, nor skip recording it.
320
+ await expect(backend.load()).rejects.toThrow('tokenizer 404');
321
+ expect(backend.status).toBe('error');
322
+ });
323
+ it('refuses a turn before load, naming what is missing', async () => {
324
+ withWebGpu();
325
+ const { options, d } = makeModules();
326
+ const backend = createBitGpuInferenceBackend(options);
327
+ await expectPathError(() => backend.chat([...USER]), 'no_usable_backend');
328
+ expect(d.sends).toEqual([]);
329
+ });
330
+ it('forwards messages and options, and maps the result', async () => {
331
+ withWebGpu();
332
+ const { options, d } = makeModules();
333
+ const backend = createBitGpuInferenceBackend(options);
334
+ await backend.load();
335
+ const response = await backend.chat([...USER], {
336
+ maxTokens: 64,
337
+ temperature: 0.4,
338
+ topP: 0.9,
339
+ stop: 'END',
340
+ });
341
+ expect(d.sends[0].messages).toEqual([{ role: 'user', content: 'hi' }]);
342
+ expect(d.sends[0].options).toMatchObject({
343
+ maxTokens: 64,
344
+ temperature: 0.4,
345
+ topP: 0.9,
346
+ // bitgpu takes an array of stop sequences; a scalar is normalized.
347
+ stopSequences: ['END'],
348
+ });
349
+ expect(response).toEqual({ content: 'reply', finishReason: 'stop' });
350
+ });
351
+ it('omits an abort finish reason the mirrored response has no member for', async () => {
352
+ withWebGpu();
353
+ const { options, d } = makeModules();
354
+ const backend = createBitGpuInferenceBackend(options);
355
+ await backend.load();
356
+ d.finishReason = 'abort';
357
+ // `AIResponse.finishReason` has no `'abort'` member — a completed response
358
+ // simply carries none rather than inventing one.
359
+ await expect(backend.chat([...USER])).resolves.toEqual({
360
+ content: 'reply',
361
+ });
362
+ });
363
+ it('forwards the caller signal — the one thing WebLLM cannot do', async () => {
364
+ withWebGpu();
365
+ const { options, d } = makeModules();
366
+ const backend = createBitGpuInferenceBackend(options);
367
+ await backend.load();
368
+ const controller = new AbortController();
369
+ for await (const _chunk of backend.stream([...USER], {
370
+ signal: controller.signal,
371
+ })) {
372
+ /* drain */
373
+ }
374
+ expect(d.streams[0].options).toMatchObject({ signal: controller.signal });
375
+ });
376
+ it('turns a timeout into a signal the engine can abort on', async () => {
377
+ withWebGpu();
378
+ const { options, d } = makeModules();
379
+ const backend = createBitGpuInferenceBackend(options);
380
+ await backend.load();
381
+ // bitgpu is the only local backend that can cancel, so a deadline must
382
+ // reach the engine rather than being silently dropped.
383
+ await backend.chat([...USER], { timeout: 5_000 });
384
+ const sent = d.sends.at(-1)?.options;
385
+ expect(sent?.signal).toBeInstanceOf(AbortSignal);
386
+ });
387
+ it('streams deltas in order and reports each to onProgress', async () => {
388
+ withWebGpu();
389
+ const { options } = makeModules();
390
+ const backend = createBitGpuInferenceBackend(options);
391
+ await backend.load();
392
+ const onProgress = vi.fn();
393
+ const chunks = [];
394
+ for await (const chunk of backend.stream([...USER], { onProgress })) {
395
+ chunks.push(chunk);
396
+ }
397
+ expect(chunks).toEqual(['Hel', 'lo']);
398
+ expect(onProgress.mock.calls).toEqual([['Hel'], ['lo']]);
399
+ });
400
+ it('refuses tool definitions and tool turns, like every other backend', async () => {
401
+ withWebGpu();
402
+ const { options, d } = makeModules();
403
+ const backend = createBitGpuInferenceBackend(options);
404
+ await backend.load();
405
+ // bitgpu supports tools, but exposing them here would mean a per-call switch
406
+ // changes what a turn means: the other backends refuse these.
407
+ await expectPathError(() => backend.chat([...USER], {
408
+ tools: [{ type: 'function', function: { name: 'product_list' } }],
409
+ }), 'unsupported_message');
410
+ await expectPathError(() => backend.chat([
411
+ {
412
+ role: 'assistant',
413
+ content: '',
414
+ tool_calls: [
415
+ {
416
+ id: 'c1',
417
+ type: 'function',
418
+ function: { name: 'x', arguments: '{}' },
419
+ },
420
+ ],
421
+ },
422
+ ]), 'unsupported_message');
423
+ expect(d.sends).toEqual([]);
424
+ });
425
+ it('disposes the engine and returns to idle on unload', async () => {
426
+ withWebGpu();
427
+ const { options, d } = makeModules();
428
+ const backend = createBitGpuInferenceBackend(options);
429
+ await backend.load();
430
+ await backend.unload();
431
+ expect(d.disposed).toBe(1);
432
+ expect(backend.status).toBe('idle');
433
+ expect(backend.progress).toBeUndefined();
434
+ });
435
+ it('refuses a turn after unload', async () => {
436
+ withWebGpu();
437
+ const { options } = makeModules();
438
+ const backend = createBitGpuInferenceBackend(options);
439
+ await backend.load();
440
+ await backend.unload();
441
+ await expectPathError(() => backend.chat([...USER]), 'no_usable_backend');
442
+ });
443
+ it('notifies subscribers on each status change', async () => {
444
+ withWebGpu();
445
+ const { options } = makeModules();
446
+ const backend = createBitGpuInferenceBackend(options);
447
+ const listener = vi.fn();
448
+ const unsubscribe = backend.subscribe(listener);
449
+ await backend.load();
450
+ await backend.unload();
451
+ // Three status changes: idle→loading, loading→ready, ready→idle.
452
+ expect(listener).toHaveBeenCalledTimes(3);
453
+ unsubscribe();
454
+ });
455
+ it('stops notifying after unsubscribe', async () => {
456
+ withWebGpu();
457
+ const { options } = makeModules();
458
+ const backend = createBitGpuInferenceBackend(options);
459
+ const listener = vi.fn();
460
+ backend.subscribe(listener)();
461
+ await backend.load();
462
+ expect(listener).not.toHaveBeenCalled();
463
+ });
464
+ });
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * LLM adapters
3
3
  */
4
+ export * from './bitgpu-inference-backend.js';
4
5
  export * from './factory.js';
5
6
  export * from './types.js';
6
7
  export { WebLLMAdapter } from './webllm.js';
8
+ export * from './webllm-inference-backend.js';
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/llm/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/llm/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,+BAA+B,CAAC;AAC9C,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,cAAc,+BAA+B,CAAC"}
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * LLM adapters
3
3
  */
4
+ export * from './bitgpu-inference-backend.js';
4
5
  export * from './factory.js';
5
6
  export * from './types.js';
6
7
  export { WebLLMAdapter } from './webllm.js';
8
+ export * from './webllm-inference-backend.js';
@@ -135,6 +135,24 @@ export interface WebLLMOptions extends BaseBrowserAIOptions {
135
135
  * dependency's type into this package's public surface.
136
136
  */
137
137
  appConfig?: Record<string, unknown>;
138
+ /**
139
+ * Supply the `@mlc-ai/web-llm` module namespace.
140
+ *
141
+ * The default loader resolves the package through a variable specifier so
142
+ * this module never hard-depends on an optional peer — but a variable
143
+ * specifier is exactly what a BROWSER cannot resolve, so the default only
144
+ * works under a bundler that rewrites it. A host that has the dependency
145
+ * should inject its own loader with a STATIC specifier:
146
+ *
147
+ * ```ts
148
+ * createWebLlmInferenceBackend({ loadModule: () => import('@mlc-ai/web-llm') })
149
+ * ```
150
+ *
151
+ * Typed as `unknown` (the module namespace) for the same reason `appConfig`
152
+ * is opaque: importing the peer's type would put it on this package's public
153
+ * surface. The adapter narrows it at the point of use.
154
+ */
155
+ loadModule?: () => Promise<unknown>;
138
156
  }
139
157
  /**
140
158
  * Transformers.js LLM adapter options
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/llm/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/llm/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * An `InferenceBackend` over a browser LLM adapter.
3
+ *
4
+ * This is the local half of the inference path: it exposes whatever the
5
+ * adapter layer can actually do — text generation with load/progress — and
6
+ * refuses, loudly, the shapes it cannot faithfully represent rather than
7
+ * quietly degrading them.
8
+ *
9
+ * ## What the local backend cannot do, and says so
10
+ *
11
+ * `LLMAdapter` (`./types.js`) is a TEXT conversation contract:
12
+ * `LLMMessage.role` is `'system' | 'user' | 'assistant'`, `content` is a
13
+ * `string`, and `LLMChatOptions` carries no `tools`. So this backend runs
14
+ * {@link assertTextOnlyTurn} — the SAME check the route backend runs — and
15
+ * throws `InferencePathError` (`code: 'unsupported_message'`) for a
16
+ * `tool`/`function` message, a message carrying `tool_calls`, non-string
17
+ * content, or any tool definition/choice. Those turns belong on a backend that
18
+ * supports them: a silent rewrite would let a model answer as though it had
19
+ * seen context (or tools) it never received, and the two backends must agree
20
+ * on which turns they accept or a per-call switch changes the answer.
21
+ *
22
+ * `signal` and `timeout` are NOT honored: the adapter has no cancellation
23
+ * seam, so a local generation runs to completion once started. That wastes GPU
24
+ * time but cannot produce a wrong answer, so it degrades rather than throws.
25
+ *
26
+ * ## Adapter ownership
27
+ *
28
+ * Pass `adapter` to share the app-managed one (the warm-client cache and
29
+ * `SmrtAppStateManager.initializeLLM` own a single adapter per
30
+ * `type:model` — `src/state/warm-clients.ts`). Omitting it constructs an
31
+ * unshared adapter via `getLLM()`, which will NOT deduplicate against that
32
+ * cache; this module cannot reach the cache itself, because `state/` depends
33
+ * on `browser-ai/` and not the other way round.
34
+ *
35
+ * @module
36
+ */
37
+ import { type InferenceBackend } from '@happyvertical/smrt-web/ai';
38
+ import type { LLMAdapter } from './types.js';
39
+ export interface WebLlmInferenceBackendOptions {
40
+ /**
41
+ * The adapter to drive. Supply the app-state-managed adapter so loading
42
+ * shares the warm-client cache; omit to construct an unshared one.
43
+ */
44
+ adapter?: LLMAdapter;
45
+ /** Backend id. Defaults to `'local'`. */
46
+ id?: string;
47
+ /** Model to ensure on `load()`. Adapter default when omitted. */
48
+ model?: string;
49
+ /** Passed through to `getLLM()` when no `adapter` is supplied. */
50
+ appConfig?: Record<string, unknown>;
51
+ /**
52
+ * Supplies the `@mlc-ai/web-llm` module namespace, passed through to the
53
+ * adapter. A browser can only resolve a STATIC specifier, so a host that has
54
+ * the dependency should pass `() => import('@mlc-ai/web-llm')`.
55
+ */
56
+ loadModule?: () => Promise<unknown>;
57
+ }
58
+ /**
59
+ * A local backend over a browser LLM adapter.
60
+ *
61
+ * `status` reports `'unavailable'` without WebGPU, so an `auto` path skips it
62
+ * instead of attempting a multi-gigabyte download on a device that cannot run
63
+ * the result. `load()` must be called (by a control, or by the caller) before
64
+ * the backend reports `'ready'`.
65
+ */
66
+ export declare function createWebLlmInferenceBackend(options?: WebLlmInferenceBackendOptions): InferenceBackend;
67
+ //# sourceMappingURL=webllm-inference-backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webllm-inference-backend.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/llm/webllm-inference-backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAEL,KAAK,gBAAgB,EAOtB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EACV,UAAU,EAIX,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,yCAAyC;IACzC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACrC;AA2DD;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,GAAE,6BAAkC,GAC1C,gBAAgB,CAsRlB"}