@aparte/provider-transformers 0.7.1 → 0.8.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
@@ -12,7 +12,7 @@ npm install @aparte/provider-transformers @huggingface/transformers
12
12
  ships its own onnxruntime). `@aparte/core` is a **peer dependency**.
13
13
 
14
14
  ```ts
15
- import { AparteConfig, DirectTransport } from '@aparte/core';
15
+ import { aparteGlobalConfig, AparteDirectTransport } from '@aparte/core';
16
16
  import { TransformersProvider, registerModel } from '@aparte/provider-transformers';
17
17
 
18
18
  registerModel({
@@ -22,14 +22,36 @@ registerModel({
22
22
  capabilities: ['streaming'],
23
23
  dtype: 'q4',
24
24
  });
25
- AparteConfig.registerAIProvider(TransformersProvider);
26
- AparteConfig.setTransport(new DirectTransport({ byok: true }));
25
+ aparteGlobalConfig.registerAIProvider(TransformersProvider);
26
+ aparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));
27
27
  ```
28
28
 
29
- The provider owns its I/O (it runs inference locally), so `DirectTransport` just delegates to it.
29
+ The provider owns its I/O (it runs inference locally), so `AparteDirectTransport` just delegates to it.
30
30
  Model weights download once and persist in the Cache API; `prepareModel` reports progress, and
31
31
  `listCachedModels` / `deleteCachedModel` manage the on-disk cache.
32
32
 
33
+ ## One pipeline per tab
34
+
35
+ Unlike every other aparté provider, this one's state is **tab-scoped, not chat-scoped**: one
36
+ worker, one loaded model, one generate at a time. `setComputeDevice`, `setMaxCachedModels` and
37
+ `setHardwareTierModels` set it for the whole page.
38
+
39
+ That is the resource talking, not a design preference — a local model is 1–2 GB of weights and
40
+ one WebGPU pipeline, so a worker per chat would mean N copies resident in one tab. Two chats on
41
+ the **same** model is the case this is for: they share the load, for free.
42
+
43
+ Two chats on **different** models is the case that costs. They serialize on the one pipeline,
44
+ and at the default budget of one cached model each turn can evict and reload gigabytes. The
45
+ provider warns once when it sees it:
46
+
47
+ ```ts
48
+ import { setMaxCachedModels, getMaxCachedModels } from '@aparte/provider-transformers';
49
+
50
+ setMaxCachedModels(2); // keep both resident, if the machine has the memory
51
+ setMaxCachedModels(0); // no limit — you are managing memory yourself
52
+ getMaxCachedModels(); // the current budget
53
+ ```
54
+
33
55
  > **Scope (v1):** generic text-generation streaming, **browser-only** (unlike the other
34
56
  > providers — it needs WebGPU/WASM, Workers and the Cache API, so it is the one adapter that
35
57
  > does not run in Node). Tool-calling for local models is model-specific and out of scope for
package/dist/index.d.ts CHANGED
@@ -2,12 +2,32 @@
2
2
  * @aparte/provider-transformers — run LLMs 100% in the browser via Transformers.js.
3
3
  *
4
4
  * A local, keyless `AparteAIProvider`: it owns its I/O (inference runs off the main
5
- * thread in a Web Worker) so `DirectTransport` delegates to its `chat()`. Model
5
+ * thread in a Web Worker) so `AparteDirectTransport` delegates to its `chat()`. Model
6
6
  * weights download once and persist in the Cache API.
7
7
  *
8
8
  * Scope (v1): generic **text-generation** streaming. Tool-calling for local models is
9
9
  * model-specific (every family has its own wire format) and is out of scope here — the
10
10
  * app registers models and streams plain replies. Vision / embeddings can follow on demand.
11
+ *
12
+ * ## This provider's state is TAB-scoped, on purpose
13
+ *
14
+ * Everything below the "Worker bridge" heading — the worker, the loaded model, the
15
+ * generate chain — plus `setComputeDevice`, `setMaxCachedModels` and
16
+ * `setHardwareTierModels`, is module-level and therefore shared by every chat on the
17
+ * page. That is deliberate, and it is the opposite of what the rest of the suite does:
18
+ * a plugin's providers scope to one chat, this one cannot.
19
+ *
20
+ * The reason is the resource, not the design. A local model is 1–2 GB of weights and one
21
+ * WebGPU pipeline. Handing each chat its own worker would mean N copies resident in one
22
+ * tab — which is the failure this package exists to avoid, not a capability. The
23
+ * settings above describe the *machine* (which backend, how many models to keep
24
+ * cached), so per-chat values would not mean anything either.
25
+ *
26
+ * What the constraint costs: two chats on the page driving DIFFERENT local models take
27
+ * turns on one pipeline, so each turn may evict and reload gigabytes. That used to
28
+ * happen silently — a multi-second stall with nothing to read. It now warns once, from
29
+ * `chat()`, when a generate is queued for a model other than the one already in flight.
30
+ * Same model in both chats is free and correct: they share the load.
11
31
  */
12
32
  import type { AparteAIProvider, AparteAIModel } from '@aparte/core';
13
33
  export interface HardwareProfile {
@@ -60,7 +80,21 @@ export declare function getMaxCachedModels(): number;
60
80
  export type ComputeDevice = 'auto' | 'webgpu' | 'wasm';
61
81
  export declare function setComputeDevice(d: ComputeDevice): void;
62
82
  export declare function getComputeDevice(): ComputeDevice;
63
- export declare const TransformersProvider: AparteAIProvider;
83
+ /**
84
+ * Narrowed so the two members the docs tell you to CALL are not optional.
85
+ *
86
+ * `AparteAIProvider` declares `prepareModel` and `getModelStatus` optional (most
87
+ * providers have nothing to download), and widening to it made both
88
+ * possibly-undefined — so the documented `TransformersProvider.prepareModel(...)`
89
+ * needed a `!` or a guard in every strict consumer. Same technique openai-compat
90
+ * already used for its own always-present members.
91
+ *
92
+ * `chat` joined the list once `AparteAIProvider` became a union: it is optional on
93
+ * the format-adapter arm, and this provider IS its `chat()` — running inference
94
+ * locally is the whole package. Narrowing it here says so once, instead of every
95
+ * caller writing `provider.chat!(...)`.
96
+ */
97
+ export declare const TransformersProvider: AparteAIProvider & Required<Pick<AparteAIProvider, 'prepareModel' | 'getModelStatus' | 'chat'>>;
64
98
  export default TransformersProvider;
65
99
  export type { AparteAIProvider, AparteAIModel, ModelStatus, ModelLoadProgress } from '@aparte/core';
66
100
  /** Returns the modelId currently loaded in the worker's pipeline, or null. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,aAAa,EAMhB,MAAM,cAAc,CAAC;AAUtB,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC7B,kBAAkB,EAAE,MAAM,CAAC;CAC9B;AAKD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAE9F;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,eAAe,CAAC,CA8B/D;AAMD,8DAA8D;AAC9D,MAAM,WAAW,uBAAuB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IAC5C,qFAAqF;IACrF,IAAI,EAAE,iBAAiB,CAAC;IACxB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,sEAAsE;IACtE,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAQD;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAUnE;AAaD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEpD;AAED,qDAAqD;AACrD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAGvD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI,CAEvD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CAEhD;AAuMD,eAAO,MAAM,oBAAoB,EAAE,gBAiIlC,CAAC;AAEF,eAAe,oBAAoB,CAAC;AACpC,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAMpG,8EAA8E;AAC9E,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED,oFAAoF;AACpF,wBAAgB,eAAe,IAAI,IAAI,CAatC;AAED,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,MAAM,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAsDpE;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBtE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,aAAa,EAMhB,MAAM,cAAc,CAAC;AAUtB,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC7B,kBAAkB,EAAE,MAAM,CAAC;CAC9B;AAKD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAE9F;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,eAAe,CAAC,CA8B/D;AAMD,8DAA8D;AAC9D,MAAM,WAAW,uBAAuB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IAC5C,qFAAqF;IACrF,IAAI,EAAE,iBAAiB,CAAC;IACxB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,sEAAsE;IACtE,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAQD;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAUnE;AAaD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEpD;AAED,qDAAqD;AACrD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAGvD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI,CAEvD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CAEhD;AA0OD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,EAAE,gBAAgB,GAC7C,QAAQ,CAAC,IAAI,CAAC,gBAAgB,EAAE,cAAc,GAAG,gBAAgB,GAAG,MAAM,CAAC,CAmIhF,CAAC;AAEF,eAAe,oBAAoB,CAAC;AACpC,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAMpG,8EAA8E;AAC9E,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED,oFAAoF;AACpF,wBAAgB,eAAe,IAAI,IAAI,CA4BtC;AAED,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,MAAM,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAsDpE;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBtE"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { contentToText } from "@aparte/core";
1
+ import { uuid, contentToText } from "@aparte/core";
2
2
  let _hardwareTiers = null;
3
3
  function setHardwareTierModels(tiers) {
4
4
  _hardwareTiers = tiers;
@@ -107,7 +107,23 @@ const _pendingPrepares = /* @__PURE__ */ new Map();
107
107
  const _pendingGenerates = /* @__PURE__ */ new Map();
108
108
  let _generateChain = Promise.resolve();
109
109
  const _generateDoneResolvers = /* @__PURE__ */ new Map();
110
+ const _queuedModelIds = /* @__PURE__ */ new Map();
111
+ let _warnedModelContention = false;
112
+ function _contendingModelId(requested) {
113
+ for (const id of _queuedModelIds.values()) if (id !== requested) return id;
114
+ return void 0;
115
+ }
116
+ function _warnIfContended(requested) {
117
+ if (_warnedModelContention) return;
118
+ const other = _contendingModelId(requested);
119
+ if (!other) return;
120
+ _warnedModelContention = true;
121
+ console.warn(
122
+ `[Aparte] Two chats are driving different local models at once ("${requested}" behind "${other}"). Transformers.js runs one pipeline per tab, so these generates are serialized, and with a cache budget of ${_maxCachedModels} each switch can evict and reload gigabytes of weights. Point both chats at one model, or raise the budget with setMaxCachedModels(2) if the machine has the memory. This warns once.`
123
+ );
124
+ }
110
125
  function _releaseGenerateSlot(id) {
126
+ _queuedModelIds.delete(id);
111
127
  const resolve = _generateDoneResolvers.get(id);
112
128
  if (resolve) {
113
129
  _generateDoneResolvers.delete(id);
@@ -154,6 +170,7 @@ function _handleWorkerError(e) {
154
170
  }
155
171
  _generateDoneResolvers.clear();
156
172
  _generateChain = Promise.resolve();
173
+ _queuedModelIds.clear();
157
174
  _loadedModelId = null;
158
175
  _preparingModelId = null;
159
176
  try {
@@ -244,13 +261,15 @@ const TransformersProvider = {
244
261
  },
245
262
  async chat(request) {
246
263
  const messages = toMessages(request.messages);
247
- const requestId = crypto.randomUUID();
264
+ const requestId = uuid();
248
265
  const options = {
249
266
  maxTokens: request.maxTokens,
250
267
  temperature: request.temperature,
251
268
  seed: request.seed
252
269
  };
253
270
  const task = _registeredModels.get(request.modelId)?.task ?? "text-generation";
271
+ _warnIfContended(request.modelId);
272
+ _queuedModelIds.set(requestId, request.modelId);
254
273
  const prevGenerate = _generateChain;
255
274
  _generateChain = new Promise((resolveSlot) => {
256
275
  _generateDoneResolvers.set(requestId, resolveSlot);
@@ -319,7 +338,7 @@ const TransformersProvider = {
319
338
  onProgress({ status: "ready" });
320
339
  return;
321
340
  }
322
- const requestId = crypto.randomUUID();
341
+ const requestId = uuid();
323
342
  _preparingModelId = modelId;
324
343
  const task = _registeredModels.get(modelId)?.task ?? "text-generation";
325
344
  const dtype = _registeredModels.get(modelId)?.dtype;
@@ -352,6 +371,16 @@ function terminateWorker() {
352
371
  }
353
372
  }
354
373
  _pendingGenerates.clear();
374
+ for (const resolve of _generateDoneResolvers.values()) {
375
+ try {
376
+ resolve();
377
+ } catch {
378
+ }
379
+ }
380
+ _generateDoneResolvers.clear();
381
+ _generateChain = Promise.resolve();
382
+ _queuedModelIds.clear();
383
+ _warnedModelContention = false;
355
384
  }
356
385
  async function listCachedModels() {
357
386
  if (!("caches" in globalThis)) return [];
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @aparte/provider-transformers — run LLMs 100% in the browser via Transformers.js.\n *\n * A local, keyless `AparteAIProvider`: it owns its I/O (inference runs off the main\n * thread in a Web Worker) so `DirectTransport` delegates to its `chat()`. Model\n * weights download once and persist in the Cache API.\n *\n * Scope (v1): generic **text-generation** streaming. Tool-calling for local models is\n * model-specific (every family has its own wire format) and is out of scope here — the\n * app registers models and streams plain replies. Vision / embeddings can follow on demand.\n */\n\nimport type {\n AparteAIProvider,\n AparteAIModel,\n AparteChatRequest,\n AparteChatResponse,\n AparteChatMessage,\n ModelStatus,\n ModelLoadProgress,\n} from '@aparte/core';\nimport { contentToText } from '@aparte/core';\n\n/** The minimal chat shape passed to the worker (the tokenizer applies the chat template). */\ntype SimpleMessage = { role: 'user' | 'assistant' | 'system'; content: string };\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Hardware detection\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface HardwareProfile {\n hasGpu: boolean;\n ramGb: number;\n tier: 'low' | 'mid' | 'high';\n recommendedModelId: string;\n}\n\n/** Hardware-tier model overrides — set by the app via setHardwareTierModels(). */\nlet _hardwareTiers: { low: string; mid?: string; high: string } | null = null;\n\n/**\n * Set the model IDs to use per hardware tier. Call before detectHardware() is used\n * to pick a default model — the provider ships no model knowledge of its own.\n */\nexport function setHardwareTierModels(tiers: { low: string; mid?: string; high: string }): void {\n _hardwareTiers = tiers;\n}\n\nexport async function detectHardware(): Promise<HardwareProfile> {\n // navigator.deviceMemory: W3C API, Chromium only, capped at 8 GB for privacy\n // (1 | 2 | 4 | 8). Falls back to 4 on Firefox/Safari.\n const ramGb: number = (navigator as unknown as { deviceMemory?: number }).deviceMemory ?? 4;\n\n // Real WebGPU check: requestAdapter() returns null if no capable GPU is present.\n let hasGpu = false;\n if ('gpu' in navigator) {\n try {\n const adapter = await (navigator as unknown as { gpu: { requestAdapter(): Promise<unknown> } }).gpu.requestAdapter();\n hasGpu = adapter !== null;\n } catch {\n hasGpu = false;\n }\n }\n\n let tier: 'low' | 'mid' | 'high';\n if (!hasGpu || ramGb < 4) {\n tier = 'low';\n } else if (ramGb < 8) {\n tier = 'mid';\n } else {\n tier = 'high';\n }\n\n const recommendedModelId = _hardwareTiers\n ? (_hardwareTiers[tier] ?? _hardwareTiers.high ?? '')\n : '';\n\n return { hasGpu, ramGb, tier, recommendedModelId };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Model catalog — all model knowledge lives in the app, not the provider.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Configuration for a model registered with the provider. */\nexport interface TransformersModelConfig {\n id: string;\n name: string;\n description?: string;\n capabilities: AparteAIModel['capabilities'];\n /** Transformers.js pipeline task — determines the model architecture / load path. */\n task: 'text-generation';\n /** ONNX dtype or per-part dtype map (e.g. `'q4'` or `{ decoder_model_merged: 'q4' }`). */\n dtype?: string | Record<string, string>;\n /** Preferred device. Defaults to WebGPU when available, else WASM. */\n device?: 'webgpu' | 'wasm' | 'auto';\n metadata?: Record<string, unknown>;\n}\n\n/** Models registered by the app via registerModel(). */\nconst _registeredModels = new Map<string, TransformersModelConfig>();\n\n/** Mutable model list — populated by registerModel() and cache discovery. */\nlet _knownModels: AparteAIModel[] = [];\n\n/**\n * Register a model with the provider. Call before the model is used for inference.\n */\nexport function registerModel(config: TransformersModelConfig): void {\n _registeredModels.set(config.id, config);\n if (!_knownModels.find(m => m.id === config.id)) {\n _knownModels = [..._knownModels, {\n id: config.id,\n name: config.name,\n description: config.description,\n capabilities: config.capabilities,\n }];\n }\n}\n\n/** Build an AparteAIModel entry from a cache-discovered modelId not in the registry. */\nfunction _modelFromCacheEntry(modelId: string): AparteAIModel {\n const config = _registeredModels.get(modelId);\n if (config) return { id: config.id, name: config.name, description: config.description, capabilities: config.capabilities };\n const name = (modelId.split('/').pop() ?? modelId).replace(/-/g, ' ');\n return { id: modelId, name, capabilities: ['streaming'] };\n}\n\n/** Max number of models to keep in cache. 0 = unlimited. Default: 1. */\nlet _maxCachedModels = 1;\n\n/**\n * Set the maximum number of models to keep in cache. When exceeded after a new\n * model is ready, the oldest models are evicted. 0 = unlimited.\n */\nexport function setMaxCachedModels(max: number): void {\n _maxCachedModels = max;\n}\n\n/** Returns the current max-cached-models setting. */\nexport function getMaxCachedModels(): number {\n return _maxCachedModels;\n}\n\n/**\n * User's preferred compute backend for local inference.\n * 'auto' → WebGPU when available, else WASM (default)\n * 'webgpu' → force WebGPU\n * 'wasm' → force WASM CPU\n */\nexport type ComputeDevice = 'auto' | 'webgpu' | 'wasm';\nlet _computeDevice: ComputeDevice = 'auto';\n\nexport function setComputeDevice(d: ComputeDevice): void {\n _computeDevice = d;\n}\n\nexport function getComputeDevice(): ComputeDevice {\n return _computeDevice;\n}\n\n/** Evict models from cache until count <= _maxCachedModels; `keepModelId` is never evicted. */\nasync function _enforceMaxCachedModels(keepModelId: string): Promise<void> {\n if (_maxCachedModels === 0) return; // unlimited\n try {\n const cached = await listCachedModels();\n const others = cached.filter(e => e.modelId !== keepModelId);\n const excess = cached.length - _maxCachedModels;\n if (excess <= 0) return;\n // Delete the excess models (oldest first — they appear first in cache scan order).\n for (let i = 0; i < excess && i < others.length; i++) {\n await deleteCachedModel(others[i]!.modelId);\n }\n } catch { /* cache unavailable */ }\n}\n\n/** Merge cached models into _knownModels (idempotent). Called by fetchModels(). */\nasync function _refreshKnownModels(): Promise<void> {\n try {\n const cached = await listCachedModels();\n for (const entry of cached) {\n if (!_knownModels.find(m => m.id === entry.modelId)) {\n _knownModels = [..._knownModels, _modelFromCacheEntry(entry.modelId)];\n }\n }\n } catch { /* cache unavailable */ }\n}\n\n/** Warn at most once per session that tool turns were left out of the prompt. */\nlet _warnedToolTurnsDropped = false;\n\n/** AparteChatMessage[] → plain chat turns (the tokenizer's chat template does the rest). */\nfunction toMessages(messages: AparteChatMessage[]): SimpleMessage[] {\n const result: SimpleMessage[] = [];\n let droppedToolTurns = 0;\n for (const m of messages) {\n if (m.role === 'user' || m.role === 'assistant' || m.role === 'system') {\n const text = contentToText(m.content);\n if (text) result.push({ role: m.role, content: text });\n } else {\n // tool_call / tool_result are not supported by this generic provider (v1):\n // rendering them needs a model-specific tool syntax. Dropping them\n // silently meant an app with registered tools got a model that never saw\n // the call or its result, with nothing to explain the behaviour.\n droppedToolTurns++;\n }\n }\n if (droppedToolTurns > 0 && !_warnedToolTurnsDropped) {\n _warnedToolTurnsDropped = true;\n console.warn(\n `[transformers] Dropped ${droppedToolTurns} tool turn(s) from the prompt: this provider ` +\n 'does not support tool calling (v1), so the model will not see the call or its result. ' +\n 'Use an OpenAI-compatible endpoint for tools, or render the turns yourself before sending.',\n );\n }\n return result;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Worker bridge\n// ─────────────────────────────────────────────────────────────────────────────\n\nlet _worker: Worker | null = null;\n\ninterface PendingPrepare {\n modelId: string;\n onProgress: (p: ModelLoadProgress) => void;\n resolve: () => void;\n reject: (err: Error) => void;\n}\nconst _pendingPrepares = new Map<string, PendingPrepare>();\nconst _pendingGenerates = new Map<string, ReadableStreamDefaultController>();\n\n// ── Generate serialization ──────────────────────────────────────────────────\n// The worker holds ONE pipeline: two concurrent generates would corrupt each\n// other. Each chat() chains its `generate` behind the previous generate's\n// completion (gen-done / gen-error).\nlet _generateChain: Promise<void> = Promise.resolve();\nconst _generateDoneResolvers = new Map<string, () => void>();\n\n/** Settle the serialization slot for a finished generate. */\nfunction _releaseGenerateSlot(id: string): void {\n const resolve = _generateDoneResolvers.get(id);\n if (resolve) {\n _generateDoneResolvers.delete(id);\n resolve();\n }\n}\n\n/** Model known to be loaded (main-thread view). */\nlet _loadedModelId: string | null = null;\n/** Model currently being prepared (for the getModelStatus 'cached' path). */\nlet _preparingModelId: string | null = null;\n\nfunction _getWorker(): Worker {\n if (!_worker) {\n _worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });\n _worker.addEventListener('message', _handleWorkerMessage);\n _worker.addEventListener('error', _handleWorkerError);\n _worker.addEventListener('messageerror', _handleWorkerError);\n }\n return _worker;\n}\n\n/**\n * Worker crashed (uncaught error / WASM init failure / OOM). Reject every in-flight\n * prepare and close every open generate stream so the UI doesn't hang. Subsequent\n * calls rebuild the worker.\n */\nfunction _handleWorkerError(e: Event): void {\n const message = (e as ErrorEvent)?.message || 'Worker crashed unexpectedly';\n\n for (const p of _pendingPrepares.values()) {\n try { p.reject(new Error(message)); } catch { /* ignore */ }\n }\n _pendingPrepares.clear();\n\n for (const ctrl of _pendingGenerates.values()) {\n try { ctrl.enqueue({ type: 'error' as const, message }); ctrl.close(); }\n catch { /* ignore */ }\n }\n _pendingGenerates.clear();\n\n // Release every serialization slot so the generate chain doesn't deadlock.\n for (const resolve of _generateDoneResolvers.values()) {\n try { resolve(); } catch { /* ignore */ }\n }\n _generateDoneResolvers.clear();\n _generateChain = Promise.resolve();\n\n _loadedModelId = null;\n _preparingModelId = null;\n try { _worker?.terminate(); } catch { /* ignore */ }\n _worker = null;\n}\n\nfunction _handleWorkerMessage(event: MessageEvent): void {\n const msg = event.data;\n\n switch (msg.type) {\n case 'progress': {\n const pending = _pendingPrepares.get(msg.id);\n if (!pending) break;\n if (msg.status === 'ready') {\n pending.onProgress({ status: 'ready' });\n pending.resolve();\n _pendingPrepares.delete(msg.id);\n } else if (msg.status === 'loading') {\n pending.onProgress({ status: 'loading' });\n } else if (msg.status === 'cached') {\n pending.onProgress({ status: 'cached', file: msg.file, progress: msg.progress });\n } else {\n pending.onProgress({ status: 'downloading', file: msg.file, progress: msg.progress });\n }\n break;\n }\n case 'prepare-error': {\n const pending = _pendingPrepares.get(msg.id);\n if (!pending) break;\n pending.reject(new Error(msg.message));\n _pendingPrepares.delete(msg.id);\n if (_preparingModelId === pending.modelId) _preparingModelId = null;\n break;\n }\n case 'pipeline-ready': {\n _loadedModelId = msg.modelId;\n _preparingModelId = null;\n // Evict models over the cache limit, then refresh the known list.\n void _enforceMaxCachedModels(msg.modelId).then(() => _refreshKnownModels());\n break;\n }\n case 'gen-chunk': {\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue({ type: msg.chunkType as 'text' | 'thinking', delta: msg.delta });\n break;\n }\n case 'gen-done': {\n _releaseGenerateSlot(msg.id);\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue({ type: 'done' as const, ...(msg.usage ? { usage: msg.usage } : {}) });\n ctrl.close();\n _pendingGenerates.delete(msg.id);\n break;\n }\n case 'gen-error': {\n _releaseGenerateSlot(msg.id);\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue({ type: 'error' as const, message: msg.message });\n ctrl.close();\n _pendingGenerates.delete(msg.id);\n break;\n }\n }\n}\n\nexport const TransformersProvider: AparteAIProvider = {\n id: 'transformers',\n\n getMetadata() {\n return {\n id: 'transformers',\n name: 'Transformers.js',\n icon: `<svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12 2L2 7l10 5 10-5-10-5z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2 17l10 5 10-5\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2 12l10 5 10-5\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`,\n color: '#f59e0b',\n description: 'Run LLMs directly in your browser via WebGPU or WASM — no API, no key',\n hasFreeModels: true,\n isLocal: true,\n helpUrl: 'https://huggingface.co/docs/transformers.js',\n };\n },\n\n getModels(): AparteAIModel[] {\n return _knownModels;\n },\n\n async fetchModels(): Promise<AparteAIModel[]> {\n await _refreshKnownModels();\n return _knownModels;\n },\n\n async chat(request: AparteChatRequest): Promise<AparteChatResponse> {\n const messages = toMessages(request.messages);\n const requestId = crypto.randomUUID();\n const options = {\n maxTokens: request.maxTokens,\n temperature: request.temperature,\n seed: request.seed,\n };\n const task = _registeredModels.get(request.modelId)?.task ?? 'text-generation';\n\n // ── Reserve a serialization slot ─────────────────────────────────────\n // Chain this generate behind the previous one; the worker has a single\n // pipeline, so generates MUST NOT overlap.\n const prevGenerate = _generateChain;\n _generateChain = new Promise<void>((resolveSlot) => {\n _generateDoneResolvers.set(requestId, resolveSlot);\n });\n const postGenerate = (): void => {\n _getWorker().postMessage({\n type: 'generate',\n id: requestId,\n modelId: request.modelId,\n messages,\n options,\n task,\n dtype: _registeredModels.get(request.modelId)?.dtype,\n device: _computeDevice,\n });\n };\n\n if (request.stream === false) {\n return new Promise<string>((resolve, reject) => {\n let result = '';\n const fakeCtrl = {\n enqueue: (chunk: { type: string; delta?: string; message?: string }) => {\n if (chunk.type === 'text') result += chunk.delta ?? '';\n else if (chunk.type === 'done') resolve(result);\n else if (chunk.type === 'error') reject(new Error(chunk.message));\n },\n close: () => { /* no-op */ },\n } as unknown as ReadableStreamDefaultController;\n _pendingGenerates.set(requestId, fakeCtrl);\n void prevGenerate.then(postGenerate);\n });\n }\n\n return new ReadableStream({\n async start(controller) {\n _pendingGenerates.set(requestId, controller);\n await prevGenerate;\n postGenerate();\n },\n cancel() {\n _pendingGenerates.delete(requestId);\n // Actually STOP the model (not just detach the reader): tell the worker\n // to interrupt this generate. The serialization slot is still released\n // by the resulting gen-done/gen-error, so a queued generate can't start\n // before the worker has stopped this one.\n _getWorker().postMessage({ type: 'cancel', id: requestId });\n },\n });\n },\n\n async getModelStatus(modelId: string): Promise<ModelStatus> {\n if (_loadedModelId === modelId) return 'ready';\n if (_preparingModelId === modelId) return 'cached';\n if ('caches' in globalThis) {\n try {\n const encodedId = encodeURIComponent(modelId);\n const names = await caches.keys();\n for (const name of names) {\n const cache = await caches.open(name);\n const keys = await cache.keys();\n if (keys.some(r => r.url.includes(encodedId) || r.url.includes(modelId + '/'))) {\n return 'cached';\n }\n }\n } catch {\n // Cache API unavailable\n }\n }\n return 'not-downloaded';\n },\n\n async prepareModel(modelId: string, onProgress: (p: ModelLoadProgress) => void): Promise<void> {\n if (_loadedModelId === modelId) {\n onProgress({ status: 'ready' });\n return;\n }\n\n const requestId = crypto.randomUUID();\n _preparingModelId = modelId;\n\n const task = _registeredModels.get(modelId)?.task ?? 'text-generation';\n const dtype = _registeredModels.get(modelId)?.dtype;\n return new Promise<void>((resolve, reject) => {\n _pendingPrepares.set(requestId, { modelId, onProgress, resolve, reject });\n _getWorker().postMessage({ type: 'prepare', id: requestId, modelId, task, dtype, device: _computeDevice });\n });\n },\n\n async deleteModel(modelId: string): Promise<void> {\n await deleteCachedModel(modelId);\n },\n};\n\nexport default TransformersProvider;\nexport type { AparteAIProvider, AparteAIModel, ModelStatus, ModelLoadProgress } from '@aparte/core';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cache utilities (settings panels, etc.)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Returns the modelId currently loaded in the worker's pipeline, or null. */\nexport function getLoadedModelId(): string | null {\n return _loadedModelId;\n}\n\n/** Terminate the shared worker and reset in-memory state. Safe to call any time. */\nexport function terminateWorker(): void {\n _worker?.terminate();\n _worker = null;\n _loadedModelId = null;\n _preparingModelId = null;\n for (const [, p] of _pendingPrepares) {\n p.reject(new Error('Worker terminated'));\n }\n _pendingPrepares.clear();\n for (const [, ctrl] of _pendingGenerates) {\n try { ctrl.enqueue({ type: 'error' as const, message: 'Worker terminated' }); ctrl.close(); } catch { /* already closed */ }\n }\n _pendingGenerates.clear();\n}\n\nexport interface CachedModelEntry {\n modelId: string;\n name: string;\n /** Total size in bytes of all cached files for this model. -1 if unknown. */\n sizeBytes: number;\n /** True if the model is currently loaded in the worker. */\n loaded: boolean;\n}\n\n/**\n * Scan the Cache API to find which Transformers.js models have been downloaded,\n * by matching cache entry URLs against the Hugging Face resolve path.\n */\nexport async function listCachedModels(): Promise<CachedModelEntry[]> {\n if (!('caches' in globalThis)) return [];\n\n const found = new Map<string, { name: string; sizeBytes: number }>();\n\n // e.g. https://huggingface.co/onnx-community/Qwen2.5-0.5B/resolve/main/config.json\n // → onnx-community/Qwen2.5-0.5B\n function extractModelId(url: string): string | null {\n const m = url.match(/huggingface\\.co\\/([^/]+\\/[^/]+)\\/resolve\\//);\n return m ? decodeURIComponent(m[1]!) : null;\n }\n\n function modelName(modelId: string): string {\n const config = _registeredModels.get(modelId);\n if (config) return config.name;\n return (modelId.split('/').pop() ?? modelId).replace(/-/g, ' ');\n }\n\n try {\n const cacheNames = await caches.keys();\n await Promise.all(cacheNames.map(async (cacheName) => {\n try {\n const cache = await caches.open(cacheName);\n const requests = await cache.keys();\n for (const req of requests) {\n const modelId = extractModelId(req.url);\n if (!modelId) continue;\n if (!found.has(modelId)) {\n found.set(modelId, { name: modelName(modelId), sizeBytes: 0 });\n }\n const response = await cache.match(req);\n if (!response) continue;\n const contentLength = response.headers.get('content-length');\n if (contentLength) {\n found.get(modelId)!.sizeBytes += parseInt(contentLength, 10);\n } else {\n try {\n const blob = await response.clone().blob();\n found.get(modelId)!.sizeBytes += blob.size;\n } catch { /* skip */ }\n }\n }\n } catch { /* skip inaccessible cache */ }\n }));\n } catch {\n return [];\n }\n\n return Array.from(found.entries()).map(([modelId, { name, sizeBytes }]) => ({\n modelId,\n name,\n sizeBytes,\n loaded: _loadedModelId === modelId,\n }));\n}\n\n/**\n * Delete all cached files for a modelId from the Cache API, terminating the worker\n * first if that model is currently loaded.\n */\nexport async function deleteCachedModel(modelId: string): Promise<void> {\n if (_loadedModelId === modelId || _preparingModelId === modelId) {\n terminateWorker();\n }\n if (!('caches' in globalThis)) return;\n try {\n const cacheNames = await caches.keys();\n await Promise.all(cacheNames.map(async (cacheName) => {\n try {\n const cache = await caches.open(cacheName);\n const requests = await cache.keys();\n const encoded = encodeURIComponent(modelId);\n await Promise.all(\n requests\n .filter(r => r.url.includes(modelId) || r.url.includes(encoded))\n .map(r => cache.delete(r)),\n );\n } catch { /* skip */ }\n }));\n } catch { /* Cache API unavailable */ }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @aparte/provider-transformers — run LLMs 100% in the browser via Transformers.js.\n *\n * A local, keyless `AparteAIProvider`: it owns its I/O (inference runs off the main\n * thread in a Web Worker) so `AparteDirectTransport` delegates to its `chat()`. Model\n * weights download once and persist in the Cache API.\n *\n * Scope (v1): generic **text-generation** streaming. Tool-calling for local models is\n * model-specific (every family has its own wire format) and is out of scope here — the\n * app registers models and streams plain replies. Vision / embeddings can follow on demand.\n *\n * ## This provider's state is TAB-scoped, on purpose\n *\n * Everything below the \"Worker bridge\" heading — the worker, the loaded model, the\n * generate chain — plus `setComputeDevice`, `setMaxCachedModels` and\n * `setHardwareTierModels`, is module-level and therefore shared by every chat on the\n * page. That is deliberate, and it is the opposite of what the rest of the suite does:\n * a plugin's providers scope to one chat, this one cannot.\n *\n * The reason is the resource, not the design. A local model is 1–2 GB of weights and one\n * WebGPU pipeline. Handing each chat its own worker would mean N copies resident in one\n * tab — which is the failure this package exists to avoid, not a capability. The\n * settings above describe the *machine* (which backend, how many models to keep\n * cached), so per-chat values would not mean anything either.\n *\n * What the constraint costs: two chats on the page driving DIFFERENT local models take\n * turns on one pipeline, so each turn may evict and reload gigabytes. That used to\n * happen silently — a multi-second stall with nothing to read. It now warns once, from\n * `chat()`, when a generate is queued for a model other than the one already in flight.\n * Same model in both chats is free and correct: they share the load.\n */\n\nimport type {\n AparteAIProvider,\n AparteAIModel,\n AparteChatRequest,\n AparteChatResponse,\n AparteChatMessage,\n ModelStatus,\n ModelLoadProgress,\n} from '@aparte/core';\nimport { contentToText, uuid } from '@aparte/core';\n\n/** The minimal chat shape passed to the worker (the tokenizer applies the chat template). */\ntype SimpleMessage = { role: 'user' | 'assistant' | 'system'; content: string };\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Hardware detection\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface HardwareProfile {\n hasGpu: boolean;\n ramGb: number;\n tier: 'low' | 'mid' | 'high';\n recommendedModelId: string;\n}\n\n/** Hardware-tier model overrides — set by the app via setHardwareTierModels(). */\nlet _hardwareTiers: { low: string; mid?: string; high: string } | null = null;\n\n/**\n * Set the model IDs to use per hardware tier. Call before detectHardware() is used\n * to pick a default model — the provider ships no model knowledge of its own.\n */\nexport function setHardwareTierModels(tiers: { low: string; mid?: string; high: string }): void {\n _hardwareTiers = tiers;\n}\n\nexport async function detectHardware(): Promise<HardwareProfile> {\n // navigator.deviceMemory: W3C API, Chromium only, capped at 8 GB for privacy\n // (1 | 2 | 4 | 8). Falls back to 4 on Firefox/Safari.\n const ramGb: number = (navigator as unknown as { deviceMemory?: number }).deviceMemory ?? 4;\n\n // Real WebGPU check: requestAdapter() returns null if no capable GPU is present.\n let hasGpu = false;\n if ('gpu' in navigator) {\n try {\n const adapter = await (navigator as unknown as { gpu: { requestAdapter(): Promise<unknown> } }).gpu.requestAdapter();\n hasGpu = adapter !== null;\n } catch {\n hasGpu = false;\n }\n }\n\n let tier: 'low' | 'mid' | 'high';\n if (!hasGpu || ramGb < 4) {\n tier = 'low';\n } else if (ramGb < 8) {\n tier = 'mid';\n } else {\n tier = 'high';\n }\n\n const recommendedModelId = _hardwareTiers\n ? (_hardwareTiers[tier] ?? _hardwareTiers.high ?? '')\n : '';\n\n return { hasGpu, ramGb, tier, recommendedModelId };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Model catalog — all model knowledge lives in the app, not the provider.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Configuration for a model registered with the provider. */\nexport interface TransformersModelConfig {\n id: string;\n name: string;\n description?: string;\n capabilities: AparteAIModel['capabilities'];\n /** Transformers.js pipeline task — determines the model architecture / load path. */\n task: 'text-generation';\n /** ONNX dtype or per-part dtype map (e.g. `'q4'` or `{ decoder_model_merged: 'q4' }`). */\n dtype?: string | Record<string, string>;\n /** Preferred device. Defaults to WebGPU when available, else WASM. */\n device?: 'webgpu' | 'wasm' | 'auto';\n metadata?: Record<string, unknown>;\n}\n\n/** Models registered by the app via registerModel(). */\nconst _registeredModels = new Map<string, TransformersModelConfig>();\n\n/** Mutable model list — populated by registerModel() and cache discovery. */\nlet _knownModels: AparteAIModel[] = [];\n\n/**\n * Register a model with the provider. Call before the model is used for inference.\n */\nexport function registerModel(config: TransformersModelConfig): void {\n _registeredModels.set(config.id, config);\n if (!_knownModels.find(m => m.id === config.id)) {\n _knownModels = [..._knownModels, {\n id: config.id,\n name: config.name,\n description: config.description,\n capabilities: config.capabilities,\n }];\n }\n}\n\n/** Build an AparteAIModel entry from a cache-discovered modelId not in the registry. */\nfunction _modelFromCacheEntry(modelId: string): AparteAIModel {\n const config = _registeredModels.get(modelId);\n if (config) return { id: config.id, name: config.name, description: config.description, capabilities: config.capabilities };\n const name = (modelId.split('/').pop() ?? modelId).replace(/-/g, ' ');\n return { id: modelId, name, capabilities: ['streaming'] };\n}\n\n/** Max number of models to keep in cache. 0 = unlimited. Default: 1. */\nlet _maxCachedModels = 1;\n\n/**\n * Set the maximum number of models to keep in cache. When exceeded after a new\n * model is ready, the oldest models are evicted. 0 = unlimited.\n */\nexport function setMaxCachedModels(max: number): void {\n _maxCachedModels = max;\n}\n\n/** Returns the current max-cached-models setting. */\nexport function getMaxCachedModels(): number {\n return _maxCachedModels;\n}\n\n/**\n * User's preferred compute backend for local inference.\n * 'auto' → WebGPU when available, else WASM (default)\n * 'webgpu' → force WebGPU\n * 'wasm' → force WASM CPU\n */\nexport type ComputeDevice = 'auto' | 'webgpu' | 'wasm';\nlet _computeDevice: ComputeDevice = 'auto';\n\nexport function setComputeDevice(d: ComputeDevice): void {\n _computeDevice = d;\n}\n\nexport function getComputeDevice(): ComputeDevice {\n return _computeDevice;\n}\n\n/** Evict models from cache until count <= _maxCachedModels; `keepModelId` is never evicted. */\nasync function _enforceMaxCachedModels(keepModelId: string): Promise<void> {\n if (_maxCachedModels === 0) return; // unlimited\n try {\n const cached = await listCachedModels();\n const others = cached.filter(e => e.modelId !== keepModelId);\n const excess = cached.length - _maxCachedModels;\n if (excess <= 0) return;\n // Delete the excess models (oldest first — they appear first in cache scan order).\n for (let i = 0; i < excess && i < others.length; i++) {\n await deleteCachedModel(others[i]!.modelId);\n }\n } catch { /* cache unavailable */ }\n}\n\n/** Merge cached models into _knownModels (idempotent). Called by fetchModels(). */\nasync function _refreshKnownModels(): Promise<void> {\n try {\n const cached = await listCachedModels();\n for (const entry of cached) {\n if (!_knownModels.find(m => m.id === entry.modelId)) {\n _knownModels = [..._knownModels, _modelFromCacheEntry(entry.modelId)];\n }\n }\n } catch { /* cache unavailable */ }\n}\n\n/** Warn at most once per session that tool turns were left out of the prompt. */\nlet _warnedToolTurnsDropped = false;\n\n/** AparteChatMessage[] → plain chat turns (the tokenizer's chat template does the rest). */\nfunction toMessages(messages: AparteChatMessage[]): SimpleMessage[] {\n const result: SimpleMessage[] = [];\n let droppedToolTurns = 0;\n for (const m of messages) {\n if (m.role === 'user' || m.role === 'assistant' || m.role === 'system') {\n const text = contentToText(m.content);\n if (text) result.push({ role: m.role, content: text });\n } else {\n // tool_call / tool_result are not supported by this generic provider (v1):\n // rendering them needs a model-specific tool syntax. Dropping them\n // silently meant an app with registered tools got a model that never saw\n // the call or its result, with nothing to explain the behaviour.\n droppedToolTurns++;\n }\n }\n if (droppedToolTurns > 0 && !_warnedToolTurnsDropped) {\n _warnedToolTurnsDropped = true;\n console.warn(\n `[transformers] Dropped ${droppedToolTurns} tool turn(s) from the prompt: this provider ` +\n 'does not support tool calling (v1), so the model will not see the call or its result. ' +\n 'Use an OpenAI-compatible endpoint for tools, or render the turns yourself before sending.',\n );\n }\n return result;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Worker bridge\n// ─────────────────────────────────────────────────────────────────────────────\n\nlet _worker: Worker | null = null;\n\ninterface PendingPrepare {\n modelId: string;\n onProgress: (p: ModelLoadProgress) => void;\n resolve: () => void;\n reject: (err: Error) => void;\n}\nconst _pendingPrepares = new Map<string, PendingPrepare>();\nconst _pendingGenerates = new Map<string, ReadableStreamDefaultController>();\n\n// ── Generate serialization ──────────────────────────────────────────────────\n// The worker holds ONE pipeline: two concurrent generates would corrupt each\n// other. Each chat() chains its `generate` behind the previous generate's\n// completion (gen-done / gen-error).\nlet _generateChain: Promise<void> = Promise.resolve();\nconst _generateDoneResolvers = new Map<string, () => void>();\n\n// ── Contention on the one pipeline ──────────────────────────────────────────\n// Serialization is correct but invisible: two chats driving DIFFERENT local\n// models take turns, and with `maxCachedModels` at its default of 1 each turn\n// can evict and reload gigabytes. The user sees a stall; the developer sees\n// nothing. These two track just enough to say so, once.\nconst _queuedModelIds = new Map<string, string>();\nlet _warnedModelContention = false;\n\n/** Model ids of generates currently queued or running on the single pipeline. */\nfunction _contendingModelId(requested: string): string | undefined {\n for (const id of _queuedModelIds.values()) if (id !== requested) return id;\n return undefined;\n}\n\n/**\n * Warn once when a generate has to queue behind another chat's DIFFERENT model.\n * Not a warning about switching models in one chat — that is a deliberate act\n * with visible feedback. This fires only when two are in flight at once.\n */\nfunction _warnIfContended(requested: string): void {\n if (_warnedModelContention) return;\n const other = _contendingModelId(requested);\n if (!other) return;\n _warnedModelContention = true;\n console.warn(\n `[Aparte] Two chats are driving different local models at once (\"${requested}\" behind `\n + `\"${other}\"). Transformers.js runs one pipeline per tab, so these generates are `\n + `serialized, and with a cache budget of ${_maxCachedModels} each switch can evict and `\n + `reload gigabytes of weights. Point both chats at one model, or raise the budget with `\n + `setMaxCachedModels(2) if the machine has the memory. This warns once.`,\n );\n}\n\n/** Settle the serialization slot for a finished generate. */\nfunction _releaseGenerateSlot(id: string): void {\n _queuedModelIds.delete(id);\n const resolve = _generateDoneResolvers.get(id);\n if (resolve) {\n _generateDoneResolvers.delete(id);\n resolve();\n }\n}\n\n/** Model known to be loaded (main-thread view). */\nlet _loadedModelId: string | null = null;\n/** Model currently being prepared (for the getModelStatus 'cached' path). */\nlet _preparingModelId: string | null = null;\n\nfunction _getWorker(): Worker {\n if (!_worker) {\n _worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });\n _worker.addEventListener('message', _handleWorkerMessage);\n _worker.addEventListener('error', _handleWorkerError);\n _worker.addEventListener('messageerror', _handleWorkerError);\n }\n return _worker;\n}\n\n/**\n * Worker crashed (uncaught error / WASM init failure / OOM). Reject every in-flight\n * prepare and close every open generate stream so the UI doesn't hang. Subsequent\n * calls rebuild the worker.\n */\nfunction _handleWorkerError(e: Event): void {\n const message = (e as ErrorEvent)?.message || 'Worker crashed unexpectedly';\n\n for (const p of _pendingPrepares.values()) {\n try { p.reject(new Error(message)); } catch { /* ignore */ }\n }\n _pendingPrepares.clear();\n\n for (const ctrl of _pendingGenerates.values()) {\n try { ctrl.enqueue({ type: 'error' as const, message }); ctrl.close(); }\n catch { /* ignore */ }\n }\n _pendingGenerates.clear();\n\n // Release every serialization slot so the generate chain doesn't deadlock.\n for (const resolve of _generateDoneResolvers.values()) {\n try { resolve(); } catch { /* ignore */ }\n }\n _generateDoneResolvers.clear();\n _generateChain = Promise.resolve();\n _queuedModelIds.clear();\n\n _loadedModelId = null;\n _preparingModelId = null;\n try { _worker?.terminate(); } catch { /* ignore */ }\n _worker = null;\n}\n\nfunction _handleWorkerMessage(event: MessageEvent): void {\n const msg = event.data;\n\n switch (msg.type) {\n case 'progress': {\n const pending = _pendingPrepares.get(msg.id);\n if (!pending) break;\n if (msg.status === 'ready') {\n pending.onProgress({ status: 'ready' });\n pending.resolve();\n _pendingPrepares.delete(msg.id);\n } else if (msg.status === 'loading') {\n pending.onProgress({ status: 'loading' });\n } else if (msg.status === 'cached') {\n pending.onProgress({ status: 'cached', file: msg.file, progress: msg.progress });\n } else {\n pending.onProgress({ status: 'downloading', file: msg.file, progress: msg.progress });\n }\n break;\n }\n case 'prepare-error': {\n const pending = _pendingPrepares.get(msg.id);\n if (!pending) break;\n pending.reject(new Error(msg.message));\n _pendingPrepares.delete(msg.id);\n if (_preparingModelId === pending.modelId) _preparingModelId = null;\n break;\n }\n case 'pipeline-ready': {\n _loadedModelId = msg.modelId;\n _preparingModelId = null;\n // Evict models over the cache limit, then refresh the known list.\n void _enforceMaxCachedModels(msg.modelId).then(() => _refreshKnownModels());\n break;\n }\n case 'gen-chunk': {\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue({ type: msg.chunkType as 'text' | 'thinking', delta: msg.delta });\n break;\n }\n case 'gen-done': {\n _releaseGenerateSlot(msg.id);\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue({ type: 'done' as const, ...(msg.usage ? { usage: msg.usage } : {}) });\n ctrl.close();\n _pendingGenerates.delete(msg.id);\n break;\n }\n case 'gen-error': {\n _releaseGenerateSlot(msg.id);\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue({ type: 'error' as const, message: msg.message });\n ctrl.close();\n _pendingGenerates.delete(msg.id);\n break;\n }\n }\n}\n\n/**\n * Narrowed so the two members the docs tell you to CALL are not optional.\n *\n * `AparteAIProvider` declares `prepareModel` and `getModelStatus` optional (most\n * providers have nothing to download), and widening to it made both\n * possibly-undefined — so the documented `TransformersProvider.prepareModel(...)`\n * needed a `!` or a guard in every strict consumer. Same technique openai-compat\n * already used for its own always-present members.\n *\n * `chat` joined the list once `AparteAIProvider` became a union: it is optional on\n * the format-adapter arm, and this provider IS its `chat()` — running inference\n * locally is the whole package. Narrowing it here says so once, instead of every\n * caller writing `provider.chat!(...)`.\n */\nexport const TransformersProvider: AparteAIProvider\n & Required<Pick<AparteAIProvider, 'prepareModel' | 'getModelStatus' | 'chat'>> = {\n id: 'transformers',\n\n getMetadata() {\n return {\n id: 'transformers',\n name: 'Transformers.js',\n icon: `<svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12 2L2 7l10 5 10-5-10-5z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2 17l10 5 10-5\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2 12l10 5 10-5\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`,\n color: '#f59e0b',\n description: 'Run LLMs directly in your browser via WebGPU or WASM — no API, no key',\n hasFreeModels: true,\n isLocal: true,\n helpUrl: 'https://huggingface.co/docs/transformers.js',\n };\n },\n\n getModels(): AparteAIModel[] {\n return _knownModels;\n },\n\n async fetchModels(): Promise<AparteAIModel[]> {\n await _refreshKnownModels();\n return _knownModels;\n },\n\n async chat(request: AparteChatRequest): Promise<AparteChatResponse> {\n const messages = toMessages(request.messages);\n const requestId = uuid();\n const options = {\n maxTokens: request.maxTokens,\n temperature: request.temperature,\n seed: request.seed,\n };\n const task = _registeredModels.get(request.modelId)?.task ?? 'text-generation';\n\n // ── Reserve a serialization slot ─────────────────────────────────────\n // Chain this generate behind the previous one; the worker has a single\n // pipeline, so generates MUST NOT overlap.\n _warnIfContended(request.modelId);\n _queuedModelIds.set(requestId, request.modelId);\n const prevGenerate = _generateChain;\n _generateChain = new Promise<void>((resolveSlot) => {\n _generateDoneResolvers.set(requestId, resolveSlot);\n });\n const postGenerate = (): void => {\n _getWorker().postMessage({\n type: 'generate',\n id: requestId,\n modelId: request.modelId,\n messages,\n options,\n task,\n dtype: _registeredModels.get(request.modelId)?.dtype,\n device: _computeDevice,\n });\n };\n\n if (request.stream === false) {\n return new Promise<string>((resolve, reject) => {\n let result = '';\n const fakeCtrl = {\n enqueue: (chunk: { type: string; delta?: string; message?: string }) => {\n if (chunk.type === 'text') result += chunk.delta ?? '';\n else if (chunk.type === 'done') resolve(result);\n else if (chunk.type === 'error') reject(new Error(chunk.message));\n },\n close: () => { /* no-op */ },\n } as unknown as ReadableStreamDefaultController;\n _pendingGenerates.set(requestId, fakeCtrl);\n void prevGenerate.then(postGenerate);\n });\n }\n\n return new ReadableStream({\n async start(controller) {\n _pendingGenerates.set(requestId, controller);\n await prevGenerate;\n postGenerate();\n },\n cancel() {\n _pendingGenerates.delete(requestId);\n // Actually STOP the model (not just detach the reader): tell the worker\n // to interrupt this generate. The serialization slot is still released\n // by the resulting gen-done/gen-error, so a queued generate can't start\n // before the worker has stopped this one.\n _getWorker().postMessage({ type: 'cancel', id: requestId });\n },\n });\n },\n\n async getModelStatus(modelId: string): Promise<ModelStatus> {\n if (_loadedModelId === modelId) return 'ready';\n if (_preparingModelId === modelId) return 'cached';\n if ('caches' in globalThis) {\n try {\n const encodedId = encodeURIComponent(modelId);\n const names = await caches.keys();\n for (const name of names) {\n const cache = await caches.open(name);\n const keys = await cache.keys();\n if (keys.some(r => r.url.includes(encodedId) || r.url.includes(modelId + '/'))) {\n return 'cached';\n }\n }\n } catch {\n // Cache API unavailable\n }\n }\n return 'not-downloaded';\n },\n\n async prepareModel(modelId: string, onProgress: (p: ModelLoadProgress) => void): Promise<void> {\n if (_loadedModelId === modelId) {\n onProgress({ status: 'ready' });\n return;\n }\n\n const requestId = uuid();\n _preparingModelId = modelId;\n\n const task = _registeredModels.get(modelId)?.task ?? 'text-generation';\n const dtype = _registeredModels.get(modelId)?.dtype;\n return new Promise<void>((resolve, reject) => {\n _pendingPrepares.set(requestId, { modelId, onProgress, resolve, reject });\n _getWorker().postMessage({ type: 'prepare', id: requestId, modelId, task, dtype, device: _computeDevice });\n });\n },\n\n async deleteModel(modelId: string): Promise<void> {\n await deleteCachedModel(modelId);\n },\n};\n\nexport default TransformersProvider;\nexport type { AparteAIProvider, AparteAIModel, ModelStatus, ModelLoadProgress } from '@aparte/core';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cache utilities (settings panels, etc.)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Returns the modelId currently loaded in the worker's pipeline, or null. */\nexport function getLoadedModelId(): string | null {\n return _loadedModelId;\n}\n\n/** Terminate the shared worker and reset in-memory state. Safe to call any time. */\nexport function terminateWorker(): void {\n _worker?.terminate();\n _worker = null;\n _loadedModelId = null;\n _preparingModelId = null;\n for (const [, p] of _pendingPrepares) {\n p.reject(new Error('Worker terminated'));\n }\n _pendingPrepares.clear();\n for (const [, ctrl] of _pendingGenerates) {\n try { ctrl.enqueue({ type: 'error' as const, message: 'Worker terminated' }); ctrl.close(); } catch { /* already closed */ }\n }\n _pendingGenerates.clear();\n\n // Release every serialization slot and reset the chain — the same three lines\n // the worker-error handler above already carried, with the same reason. Without\n // them, terminating mid-generate left `_generateChain` pending on a resolver\n // that had just been dropped, so the NEXT chat() awaited a promise that could\n // never settle: no error, no rejection, the stream simply never started again\n // for the life of the page.\n for (const resolve of _generateDoneResolvers.values()) {\n try { resolve(); } catch { /* ignore */ }\n }\n _generateDoneResolvers.clear();\n _generateChain = Promise.resolve();\n _queuedModelIds.clear();\n // A terminated worker is a fresh situation; let the contention warning speak again.\n _warnedModelContention = false;\n}\n\nexport interface CachedModelEntry {\n modelId: string;\n name: string;\n /** Total size in bytes of all cached files for this model. -1 if unknown. */\n sizeBytes: number;\n /** True if the model is currently loaded in the worker. */\n loaded: boolean;\n}\n\n/**\n * Scan the Cache API to find which Transformers.js models have been downloaded,\n * by matching cache entry URLs against the Hugging Face resolve path.\n */\nexport async function listCachedModels(): Promise<CachedModelEntry[]> {\n if (!('caches' in globalThis)) return [];\n\n const found = new Map<string, { name: string; sizeBytes: number }>();\n\n // e.g. https://huggingface.co/onnx-community/Qwen2.5-0.5B/resolve/main/config.json\n // → onnx-community/Qwen2.5-0.5B\n function extractModelId(url: string): string | null {\n const m = url.match(/huggingface\\.co\\/([^/]+\\/[^/]+)\\/resolve\\//);\n return m ? decodeURIComponent(m[1]!) : null;\n }\n\n function modelName(modelId: string): string {\n const config = _registeredModels.get(modelId);\n if (config) return config.name;\n return (modelId.split('/').pop() ?? modelId).replace(/-/g, ' ');\n }\n\n try {\n const cacheNames = await caches.keys();\n await Promise.all(cacheNames.map(async (cacheName) => {\n try {\n const cache = await caches.open(cacheName);\n const requests = await cache.keys();\n for (const req of requests) {\n const modelId = extractModelId(req.url);\n if (!modelId) continue;\n if (!found.has(modelId)) {\n found.set(modelId, { name: modelName(modelId), sizeBytes: 0 });\n }\n const response = await cache.match(req);\n if (!response) continue;\n const contentLength = response.headers.get('content-length');\n if (contentLength) {\n found.get(modelId)!.sizeBytes += parseInt(contentLength, 10);\n } else {\n try {\n const blob = await response.clone().blob();\n found.get(modelId)!.sizeBytes += blob.size;\n } catch { /* skip */ }\n }\n }\n } catch { /* skip inaccessible cache */ }\n }));\n } catch {\n return [];\n }\n\n return Array.from(found.entries()).map(([modelId, { name, sizeBytes }]) => ({\n modelId,\n name,\n sizeBytes,\n loaded: _loadedModelId === modelId,\n }));\n}\n\n/**\n * Delete all cached files for a modelId from the Cache API, terminating the worker\n * first if that model is currently loaded.\n */\nexport async function deleteCachedModel(modelId: string): Promise<void> {\n if (_loadedModelId === modelId || _preparingModelId === modelId) {\n terminateWorker();\n }\n if (!('caches' in globalThis)) return;\n try {\n const cacheNames = await caches.keys();\n await Promise.all(cacheNames.map(async (cacheName) => {\n try {\n const cache = await caches.open(cacheName);\n const requests = await cache.keys();\n const encoded = encodeURIComponent(modelId);\n await Promise.all(\n requests\n .filter(r => r.url.includes(modelId) || r.url.includes(encoded))\n .map(r => cache.delete(r)),\n );\n } catch { /* skip */ }\n }));\n } catch { /* Cache API unavailable */ }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aparte/provider-transformers",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Run LLMs 100% in the browser via Transformers.js (WebGPU/WASM) — a local, keyless AI provider for aparté. Streams tokens off the main thread in a Web Worker.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -24,7 +24,7 @@
24
24
  "node": ">=18"
25
25
  },
26
26
  "peerDependencies": {
27
- "@aparte/core": ">=0.5.0-alpha.0 <1.0.0",
27
+ "@aparte/core": ">=0.7.0 <1.0.0",
28
28
  "@huggingface/transformers": "^4.2.0"
29
29
  },
30
30
  "devDependencies": {
@@ -32,8 +32,7 @@
32
32
  "@types/node": "^22.0.0",
33
33
  "typescript": "^5.4.0",
34
34
  "vite": "^6.0.0",
35
- "vite-plugin-dts": "^4.5.4",
36
- "@aparte/core": "0.7.1"
35
+ "@aparte/core": "0.8.0"
37
36
  },
38
37
  "keywords": [
39
38
  "ai",