@aparte/provider-transformers 0.16.11 → 0.17.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
@@ -56,9 +56,11 @@ registerModel({
56
56
 
57
57
  Measured: SmolVLM-256M on WebGPU (Chromium, AMD Radeon 8060S): first load 7 s (download included), first token 3.7 s cold; Stop interrupts the model, not just the read.
58
58
 
59
- Each runner is its own chunk, loaded only when a model asks for it. Both drop `tool_call` /
60
- `tool_result` turns with one warning (tool syntax is per model family), and the text runner
61
- **says so when it drops an image** — it never answers a photo it could not see as if it had.
59
+ Each runner is its own chunk, loaded only when a model asks for it. Both drop the tool CALLS and
60
+ their results — the calls on an assistant turn and the `tool` turns that answer them — with one
61
+ warning (tool syntax is per model family); what the assistant *said* before calling stays in the
62
+ prompt. The text runner **says so when it drops an image**: it never answers a photo it could not
63
+ see as if it had, and the vision runner counts a content part it cannot carry the same way.
62
64
 
63
65
  ### A runner of your own
64
66
 
@@ -133,7 +135,7 @@ getMaxCachedModels(); // the current budget
133
135
  > **Scope:** text and vision models through the built-in runners, anything else through a
134
136
  > runner of your own; **browser-only** (unlike the other providers — it needs WebGPU/WASM,
135
137
  > Workers and the Cache API, so it is the one adapter that does not run in Node). Tool-calling
136
- > for local models is model-specific: the built-in runners drop `tool_call` / `tool_result`
137
- > turns with one console warning; a custom runner may render them. Part of the
138
- > [aparté](https://github.com/apartejs/aparte) monorepo. ESM-only.
138
+ > for local models is model-specific: the built-in runners drop the tool calls and their results —
139
+ > the calls on an assistant turn and the `tool` turns that answer them, not what the assistant said
140
+ > before them — with one console warning; a custom runner may render them. Part of the [aparté](https://github.com/apartejs/aparte) monorepo. ESM-only.
139
141
  > See the **Providers** guide in the docs for the full usage.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -62,7 +62,8 @@ async function _enforceMaxCachedModels(keepModelId) {
62
62
  if (_maxCachedModels === 0) return;
63
63
  try {
64
64
  const cached = await listCachedModels();
65
- const others = cached.filter((e) => e.modelId !== keepModelId);
65
+ const inUse = new Set(_queuedModelIds.values());
66
+ const others = cached.filter((e) => e.modelId !== keepModelId && !inUse.has(e.modelId));
66
67
  const excess = cached.length - _maxCachedModels;
67
68
  if (excess <= 0) return;
68
69
  for (let i = 0; i < excess && i < others.length; i++) {
@@ -327,12 +328,17 @@ const TransformersProvider = {
327
328
  if (stopped) return;
328
329
  stopped = true;
329
330
  signal?.removeEventListener("abort", stop);
331
+ const ctrl = _pendingGenerates.get(requestId);
332
+ _pendingGenerates.delete(requestId);
330
333
  if (posted) {
331
334
  _getWorker().postMessage({ type: "cancel", id: requestId });
335
+ try {
336
+ ctrl?.enqueue({ type: "done" });
337
+ ctrl?.close();
338
+ } catch {
339
+ }
332
340
  return;
333
341
  }
334
- const ctrl = _pendingGenerates.get(requestId);
335
- _pendingGenerates.delete(requestId);
336
342
  if (!ctrl) return;
337
343
  try {
338
344
  ctrl.enqueue({ type: "error", message: "Generation cancelled before it started" });
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 `AparteDirectTransport` delegates to its `chat()`. Model\n * weights download once and persist in the Cache API.\n *\n * Scope: the worker runs a **runner** — the built-in `text-generation` (any chat model\n * behind Transformers.js' `pipeline()`), or a module of the app's own named by\n * `TransformersModelConfig.runner` (see `runners/types.ts` for the contract). Tool-calling\n * for local models is model-specific (every family has its own wire format), so the\n * built-in drops tool turns and says so; a custom runner may render them.\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 ModelStatus,\n ModelLoadProgress,\n} from '@aparte/core';\nimport { uuid } from '@aparte/core';\nimport type { BuiltInRunner, Device, Dtype } from './runners/types.js';\n\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 /**\n * Which built-in runner loads and drives the model. `'text-generation'` (the default)\n * is any chat model behind Transformers.js' `pipeline()`. Ignored when `runner` is set.\n */\n task?: BuiltInRunner;\n /**\n * A runner of your own: the URL of an ES module exporting `createRunner` (see\n * `TransformersRunner`). Resolved against the page, imported by the worker, and handed\n * the same Transformers.js instance the built-ins use. Wins over `task`.\n */\n runner?: string;\n /** ONNX dtype or per-part dtype map (e.g. `'q4'` or `{ decoder_model_merged: 'q4' }`). */\n dtype?: Dtype;\n /** Preferred device. Defaults to WebGPU when available, else WASM. */\n device?: Device;\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/**\n * How the worker should load `modelId`: which runner, which weights, which device.\n *\n * A custom `runner` is made absolute HERE, not in the worker: a worker's base URL is its\n * own script's, not the page's, and the blob shim `_spawnWorker` may build has no\n * meaningful base at all — so a relative path would resolve against the wrong place or\n * fail outright. The page is the one place that knows what the app meant.\n */\nfunction _selection(modelId: string): { task: BuiltInRunner; runner?: string; dtype?: Dtype; device: ComputeDevice } {\n const config = _registeredModels.get(modelId);\n const runner = config?.runner;\n return {\n task: config?.task ?? 'text-generation',\n ...(runner ? { runner: typeof location === 'undefined' ? runner : new URL(runner, location.href).href } : {}),\n dtype: config?.dtype,\n device: _computeDevice,\n };\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>();\nconst _pendingCommands = new Map<string, { resolve: (value: unknown) => void; reject: (err: Error) => void }>();\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\n/**\n * The worker this package publishes, beside `dist/index.js`.\n *\n * It used to be `import workerUrl from './worker.ts?worker&url'`, which handed the\n * emit to Vite's worker plugin — and that plugin rewrote the one shape a consumer's\n * bundler can detect into `new URL(\"assets/worker-<hash>.js\", import.meta.url).href`\n * behind a `@vite-ignore`. Nothing static was left to detect, so a bundled app copied\n * the chunk as an opaque asset without ever processing it as a module, and the\n * `import('@huggingface/transformers')` inside it stayed a bare specifier no browser\n * can resolve: every model load failed. The worker is a second lib entry now, at a\n * stable `dist/worker.js`, and this package constructs it itself.\n */\nconst WORKER_FILE = './worker.js';\n\n/** The blob URL the worker was built from, if it needed one. Revoked with the worker. */\nlet _workerBlobUrl: string | null = null;\n\n/**\n * Build the worker — including when this package is served from another origin.\n *\n * `new Worker()` refuses a cross-origin script outright, and that is not an exotic\n * case: it is every deploy whose JavaScript lives on a CDN or an asset host while the\n * page lives somewhere else, with or without a bundler. Reproduced with the package on\n * one port and the page on another: `SecurityError: Script at '…/assets/worker-*.js'\n * cannot be accessed from origin '…'`.\n *\n * A blob inherits the ORIGIN OF THE DOCUMENT THAT CREATES IT, so a one-line blob whose\n * body imports the real worker by absolute URL is same-origin by construction, and the\n * import inside it is a normal cross-origin module fetch, which is allowed. It is the\n * shim ffmpeg.wasm and tesseract.js use for the same reason.\n *\n * Same-origin keeps the direct path: no blob, nothing to revoke, and a stack trace that\n * names the real file.\n */\nfunction _spawnWorker(): Worker {\n // Behind a constant, and that is not style either. Vite's asset transform rewrites\n // `new URL(<literal>, import.meta.url)` to a base of its own; behind a variable it\n // does not look, so this line keeps the REAL module URL — which is what the origin\n // comparison and the blob body below have to be built from. The build drops that\n // transform anyway (see `vite.config.ts`), so in the shipped bytes the two forms\n // resolve identically; under a dev server and under vitest they do not.\n const url = new URL(WORKER_FILE, import.meta.url);\n const sameOrigin = typeof location === 'undefined' || url.origin === location.origin;\n // A blob is the only way across an origin, so an environment that cannot mint one has\n // nothing to gain from trying: construct directly and let the platform say what it\n // thinks. jsdom is that environment — it has `Blob` and no `URL.createObjectURL` — and\n // every test in this package went through the blob path and threw before this line\n // existed.\n const canMintBlob = typeof Blob === 'function' && typeof URL.createObjectURL === 'function';\n // The literal below is not style, and it is written out rather than reusing\n // `WORKER_FILE` above. `new Worker(new URL('./worker.js', import.meta.url))` is the\n // exact shape Vite's worker detection and webpack's WorkerPlugin match on, and\n // matching it is what makes a CONSUMER's bundler process the worker as a module —\n // which is how `@huggingface/transformers` gets resolved inside it. Behind a variable\n // the file is copied as an opaque asset and its imports are never touched, so\n // hoisting this line to reuse `url` would fix a CDN page by breaking every bundled\n // app. That the SHIPPED bytes still carry it is asserted by\n // `src/__tests__/published-shape.test.ts`: the claim was true of this source and\n // false of the artifact for as long as the build owned the emit.\n if (sameOrigin || !canMintBlob) return new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });\n\n _workerBlobUrl = URL.createObjectURL(\n new Blob([`import ${JSON.stringify(url.href)};`], { type: 'text/javascript' }),\n );\n try {\n return new Worker(_workerBlobUrl, { type: 'module' });\n } catch (error) {\n // A page with `worker-src 'self'` (or `script-src` without `blob:`) blocks the\n // shim, and the direct URL was already refused for its origin — so there is\n // nothing left to try. Say which of the two walls was hit, because the browser's\n // own message does not distinguish them.\n URL.revokeObjectURL(_workerBlobUrl);\n _workerBlobUrl = null;\n throw new Error(\n `@aparte/provider-transformers is served from ${url.origin}, which is not this page's origin, `\n + 'so its worker has to be started through a blob: URL — and this page\\'s Content-Security-Policy '\n + 'refuses that. Allow `blob:` in `worker-src` (or `script-src`), or serve the package from your '\n + `own origin. Original error: ${String(error)}`,\n );\n }\n}\n\nfunction _releaseWorkerBlob(): void {\n if (_workerBlobUrl) {\n URL.revokeObjectURL(_workerBlobUrl);\n _workerBlobUrl = null;\n }\n}\n\n/**\n * Where the page says Transformers.js lives, if it says so at all.\n *\n * The worker cannot ask: an import map is the DOCUMENT's, and by spec it does not reach\n * a worker. The main thread can, and does it the platform's way — `import.meta.resolve`\n * consults that same map — so a page that already maps `@huggingface/transformers` (it\n * has to, to import this package by name at all) is telling us where its copy is. That\n * map is the CDN consumer's manifest: the version pin stays with the consumer, which is\n * the whole point of a peer dependency, and this package invents no second place to say\n * it.\n *\n * `undefined` under a bundler, where the specifier is resolved at build time and the\n * worker's own `import('@huggingface/transformers')` is the path that runs.\n */\nfunction _peerModuleUrl(): string | undefined {\n const resolve = (import.meta as unknown as { resolve?: (specifier: string) => string }).resolve;\n if (typeof resolve === 'function') {\n try {\n const href = resolve('@huggingface/transformers');\n if (href && /^https?:/i.test(href)) return href;\n } catch { /* not in the map — fall through */ }\n }\n // Older engines have no `import.meta.resolve`; read the map they do have.\n try {\n const el = document.querySelector('script[type=\"importmap\"]');\n const map = el?.textContent ? JSON.parse(el.textContent) as { imports?: Record<string, string> } : null;\n const href = map?.imports?.['@huggingface/transformers'];\n if (href) return new URL(href, location.href).href;\n } catch { /* no document, or a map that is not JSON */ }\n return undefined;\n}\n\nfunction _getWorker(): Worker {\n if (!_worker) {\n _worker = _spawnWorker();\n _worker.addEventListener('message', _handleWorkerMessage);\n _worker.addEventListener('error', _handleWorkerError);\n _worker.addEventListener('messageerror', _handleWorkerError);\n // First message, before any work: postMessage keeps order, so the worker has it\n // by the time a prepare or a generate needs the module.\n _worker.postMessage({ type: 'init', transformersUrl: _peerModuleUrl() });\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 for (const c of _pendingCommands.values()) c.reject(new Error(message));\n _pendingCommands.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 _releaseWorkerBlob();\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-event': {\n // The runner speaks the stream vocabulary itself; nothing to translate.\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue(msg.event);\n break;\n }\n case 'warning': {\n // Already said once per text by the worker; the page just carries the voice.\n console.warn(`[transformers] ${msg.message}`);\n break;\n }\n case 'command-result': {\n _releaseGenerateSlot(msg.id);\n const pending = _pendingCommands.get(msg.id);\n if (!pending) break;\n _pendingCommands.delete(msg.id);\n if (msg.error !== undefined) pending.reject(new Error(msg.error));\n else pending.resolve(msg.result);\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(\n request: AparteChatRequest,\n _config?: string | Record<string, string>,\n ctx?: { providerId: string; signal?: AbortSignal },\n ): Promise<AparteChatResponse> {\n const requestId = uuid();\n const options = {\n maxTokens: request.maxTokens,\n temperature: request.temperature,\n seed: request.seed,\n };\n const signal = ctx?.signal;\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 // ── Stop, from either side ───────────────────────────────────────────\n // The transport's `ctx.signal` (the user's Stop, which the provider contract\n // says a bridge MUST honour — this one read it nowhere) and the stream's own\n // `cancel()` say the same thing, and the worker hears it once. Before the\n // generate has been posted there is nothing to interrupt: the stream is\n // settled here, and the slot is released when its turn in the chain comes —\n // not earlier, or the next generate would start over the one still running.\n let posted = false;\n let stopped = false;\n const stop = (): void => {\n if (stopped) return;\n stopped = true;\n signal?.removeEventListener('abort', stop);\n if (posted) {\n _getWorker().postMessage({ type: 'cancel', id: requestId });\n return;\n }\n const ctrl = _pendingGenerates.get(requestId);\n _pendingGenerates.delete(requestId);\n if (!ctrl) return;\n try { ctrl.enqueue({ type: 'error' as const, message: 'Generation cancelled before it started' }); ctrl.close(); }\n catch { /* already closed */ }\n };\n const postGenerate = (): void => {\n if (stopped) { _releaseGenerateSlot(requestId); return; }\n posted = true;\n _getWorker().postMessage({\n type: 'generate',\n id: requestId,\n modelId: request.modelId,\n // The conversation as it is, parts included: which parts a model can take\n // is the runner's knowledge, not this thread's.\n messages: request.messages,\n options,\n ..._selection(request.modelId),\n });\n };\n\n let response: AparteChatResponse | Promise<string>;\n if (request.stream === false) {\n response = 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 } else {\n response = new ReadableStream({\n async start(controller) {\n _pendingGenerates.set(requestId, controller);\n await prevGenerate;\n postGenerate();\n },\n cancel() {\n // The reader is gone, so nothing may be enqueued for it again — and the\n // model actually STOPS (not just the read): the worker interrupts this\n // generate, and the slot is still released by the resulting\n // gen-done/gen-error, so a queued generate cannot start before that.\n _pendingGenerates.delete(requestId);\n stop();\n },\n });\n }\n\n // `start` has run by now, so the controller is registered and a stop settles it.\n if (signal?.aborted) stop();\n else signal?.addEventListener('abort', stop, { once: true });\n return response;\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 return new Promise<void>((resolve, reject) => {\n _pendingPrepares.set(requestId, { modelId, onProgress, resolve, reject });\n _getWorker().postMessage({ type: 'prepare', id: requestId, modelId, ..._selection(modelId) });\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';\nexport type {\n TransformersRunner,\n RunnerContext,\n RunnerGenerateInput,\n RunnerProgress,\n RunnerModule,\n CreateRunner,\n BuiltInRunner,\n TransformersModule,\n} from './runners/types.js';\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/**\n * Send a runner something that is not a generation — swap an adapter, warm a cache, ask\n * a capability — and get its answer. The name and payload are the runner's vocabulary\n * (the built-in runners answer none). Queued behind the generates in flight: the worker\n * holds one runner, and a command on it mid-stream would race the stream.\n */\nexport function runnerCommand(modelId: string, name: string, payload: unknown): Promise<unknown> {\n const requestId = uuid();\n _queuedModelIds.set(requestId, modelId);\n const previous = _generateChain;\n _generateChain = new Promise<void>((resolveSlot) => {\n _generateDoneResolvers.set(requestId, resolveSlot);\n });\n return new Promise<unknown>((resolve, reject) => {\n _pendingCommands.set(requestId, { resolve, reject });\n void previous.then(() => {\n _getWorker().postMessage({ type: 'command', id: requestId, modelId, name, payload, ..._selection(modelId) });\n });\n });\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 _releaseWorkerBlob();\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 for (const c of _pendingCommands.values()) c.reject(new Error('Worker terminated'));\n _pendingCommands.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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"}
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: the worker runs a **runner** — the built-in `text-generation` (any chat model\n * behind Transformers.js' `pipeline()`), or a module of the app's own named by\n * `TransformersModelConfig.runner` (see `runners/types.ts` for the contract). Tool-calling\n * for local models is model-specific (every family has its own wire format), so the\n * built-in drops tool turns and says so; a custom runner may render them.\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 ModelStatus,\n ModelLoadProgress,\n} from '@aparte/core';\nimport { uuid } from '@aparte/core';\nimport type { BuiltInRunner, Device, Dtype } from './runners/types.js';\n\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 /**\n * Which built-in runner loads and drives the model. `'text-generation'` (the default)\n * is any chat model behind Transformers.js' `pipeline()`. Ignored when `runner` is set.\n */\n task?: BuiltInRunner;\n /**\n * A runner of your own: the URL of an ES module exporting `createRunner` (see\n * `TransformersRunner`). Resolved against the page, imported by the worker, and handed\n * the same Transformers.js instance the built-ins use. Wins over `task`.\n */\n runner?: string;\n /** ONNX dtype or per-part dtype map (e.g. `'q4'` or `{ decoder_model_merged: 'q4' }`). */\n dtype?: Dtype;\n /** Preferred device. Defaults to WebGPU when available, else WASM. */\n device?: Device;\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/**\n * Evict models from cache until count <= _maxCachedModels. `keepModelId` is never\n * evicted, and neither is a model a generate is queued or running on: the budget\n * fires from `pipeline-ready`, i.e. when ANOTHER model finishes loading, so it\n * used to delete the weights of the model that was answering — and\n * `deleteCachedModel` terminates the worker when that model is the loaded one.\n */\nasync function _enforceMaxCachedModels(keepModelId: string): Promise<void> {\n if (_maxCachedModels === 0) return; // unlimited\n try {\n const cached = await listCachedModels();\n const inUse = new Set(_queuedModelIds.values());\n const others = cached.filter(e => e.modelId !== keepModelId && !inUse.has(e.modelId));\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/**\n * How the worker should load `modelId`: which runner, which weights, which device.\n *\n * A custom `runner` is made absolute HERE, not in the worker: a worker's base URL is its\n * own script's, not the page's, and the blob shim `_spawnWorker` may build has no\n * meaningful base at all — so a relative path would resolve against the wrong place or\n * fail outright. The page is the one place that knows what the app meant.\n */\nfunction _selection(modelId: string): { task: BuiltInRunner; runner?: string; dtype?: Dtype; device: ComputeDevice } {\n const config = _registeredModels.get(modelId);\n const runner = config?.runner;\n return {\n task: config?.task ?? 'text-generation',\n ...(runner ? { runner: typeof location === 'undefined' ? runner : new URL(runner, location.href).href } : {}),\n dtype: config?.dtype,\n device: _computeDevice,\n };\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>();\nconst _pendingCommands = new Map<string, { resolve: (value: unknown) => void; reject: (err: Error) => void }>();\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\n/**\n * The worker this package publishes, beside `dist/index.js`.\n *\n * It used to be `import workerUrl from './worker.ts?worker&url'`, which handed the\n * emit to Vite's worker plugin — and that plugin rewrote the one shape a consumer's\n * bundler can detect into `new URL(\"assets/worker-<hash>.js\", import.meta.url).href`\n * behind a `@vite-ignore`. Nothing static was left to detect, so a bundled app copied\n * the chunk as an opaque asset without ever processing it as a module, and the\n * `import('@huggingface/transformers')` inside it stayed a bare specifier no browser\n * can resolve: every model load failed. The worker is a second lib entry now, at a\n * stable `dist/worker.js`, and this package constructs it itself.\n */\nconst WORKER_FILE = './worker.js';\n\n/** The blob URL the worker was built from, if it needed one. Revoked with the worker. */\nlet _workerBlobUrl: string | null = null;\n\n/**\n * Build the worker — including when this package is served from another origin.\n *\n * `new Worker()` refuses a cross-origin script outright, and that is not an exotic\n * case: it is every deploy whose JavaScript lives on a CDN or an asset host while the\n * page lives somewhere else, with or without a bundler. Reproduced with the package on\n * one port and the page on another: `SecurityError: Script at '…/assets/worker-*.js'\n * cannot be accessed from origin '…'`.\n *\n * A blob inherits the ORIGIN OF THE DOCUMENT THAT CREATES IT, so a one-line blob whose\n * body imports the real worker by absolute URL is same-origin by construction, and the\n * import inside it is a normal cross-origin module fetch, which is allowed. It is the\n * shim ffmpeg.wasm and tesseract.js use for the same reason.\n *\n * Same-origin keeps the direct path: no blob, nothing to revoke, and a stack trace that\n * names the real file.\n */\nfunction _spawnWorker(): Worker {\n // Behind a constant, and that is not style either. Vite's asset transform rewrites\n // `new URL(<literal>, import.meta.url)` to a base of its own; behind a variable it\n // does not look, so this line keeps the REAL module URL — which is what the origin\n // comparison and the blob body below have to be built from. The build drops that\n // transform anyway (see `vite.config.ts`), so in the shipped bytes the two forms\n // resolve identically; under a dev server and under vitest they do not.\n const url = new URL(WORKER_FILE, import.meta.url);\n const sameOrigin = typeof location === 'undefined' || url.origin === location.origin;\n // A blob is the only way across an origin, so an environment that cannot mint one has\n // nothing to gain from trying: construct directly and let the platform say what it\n // thinks. jsdom is that environment — it has `Blob` and no `URL.createObjectURL` — and\n // every test in this package went through the blob path and threw before this line\n // existed.\n const canMintBlob = typeof Blob === 'function' && typeof URL.createObjectURL === 'function';\n // The literal below is not style, and it is written out rather than reusing\n // `WORKER_FILE` above. `new Worker(new URL('./worker.js', import.meta.url))` is the\n // exact shape Vite's worker detection and webpack's WorkerPlugin match on, and\n // matching it is what makes a CONSUMER's bundler process the worker as a module —\n // which is how `@huggingface/transformers` gets resolved inside it. Behind a variable\n // the file is copied as an opaque asset and its imports are never touched, so\n // hoisting this line to reuse `url` would fix a CDN page by breaking every bundled\n // app. That the SHIPPED bytes still carry it is asserted by\n // `src/__tests__/published-shape.test.ts`: the claim was true of this source and\n // false of the artifact for as long as the build owned the emit.\n if (sameOrigin || !canMintBlob) return new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });\n\n _workerBlobUrl = URL.createObjectURL(\n new Blob([`import ${JSON.stringify(url.href)};`], { type: 'text/javascript' }),\n );\n try {\n return new Worker(_workerBlobUrl, { type: 'module' });\n } catch (error) {\n // A page with `worker-src 'self'` (or `script-src` without `blob:`) blocks the\n // shim, and the direct URL was already refused for its origin — so there is\n // nothing left to try. Say which of the two walls was hit, because the browser's\n // own message does not distinguish them.\n URL.revokeObjectURL(_workerBlobUrl);\n _workerBlobUrl = null;\n throw new Error(\n `@aparte/provider-transformers is served from ${url.origin}, which is not this page's origin, `\n + 'so its worker has to be started through a blob: URL — and this page\\'s Content-Security-Policy '\n + 'refuses that. Allow `blob:` in `worker-src` (or `script-src`), or serve the package from your '\n + `own origin. Original error: ${String(error)}`,\n );\n }\n}\n\nfunction _releaseWorkerBlob(): void {\n if (_workerBlobUrl) {\n URL.revokeObjectURL(_workerBlobUrl);\n _workerBlobUrl = null;\n }\n}\n\n/**\n * Where the page says Transformers.js lives, if it says so at all.\n *\n * The worker cannot ask: an import map is the DOCUMENT's, and by spec it does not reach\n * a worker. The main thread can, and does it the platform's way — `import.meta.resolve`\n * consults that same map — so a page that already maps `@huggingface/transformers` (it\n * has to, to import this package by name at all) is telling us where its copy is. That\n * map is the CDN consumer's manifest: the version pin stays with the consumer, which is\n * the whole point of a peer dependency, and this package invents no second place to say\n * it.\n *\n * `undefined` under a bundler, where the specifier is resolved at build time and the\n * worker's own `import('@huggingface/transformers')` is the path that runs.\n */\nfunction _peerModuleUrl(): string | undefined {\n const resolve = (import.meta as unknown as { resolve?: (specifier: string) => string }).resolve;\n if (typeof resolve === 'function') {\n try {\n const href = resolve('@huggingface/transformers');\n if (href && /^https?:/i.test(href)) return href;\n } catch { /* not in the map — fall through */ }\n }\n // Older engines have no `import.meta.resolve`; read the map they do have.\n try {\n const el = document.querySelector('script[type=\"importmap\"]');\n const map = el?.textContent ? JSON.parse(el.textContent) as { imports?: Record<string, string> } : null;\n const href = map?.imports?.['@huggingface/transformers'];\n if (href) return new URL(href, location.href).href;\n } catch { /* no document, or a map that is not JSON */ }\n return undefined;\n}\n\nfunction _getWorker(): Worker {\n if (!_worker) {\n _worker = _spawnWorker();\n _worker.addEventListener('message', _handleWorkerMessage);\n _worker.addEventListener('error', _handleWorkerError);\n _worker.addEventListener('messageerror', _handleWorkerError);\n // First message, before any work: postMessage keeps order, so the worker has it\n // by the time a prepare or a generate needs the module.\n _worker.postMessage({ type: 'init', transformersUrl: _peerModuleUrl() });\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 for (const c of _pendingCommands.values()) c.reject(new Error(message));\n _pendingCommands.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 _releaseWorkerBlob();\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-event': {\n // The runner speaks the stream vocabulary itself; nothing to translate.\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue(msg.event);\n break;\n }\n case 'warning': {\n // Already said once per text by the worker; the page just carries the voice.\n console.warn(`[transformers] ${msg.message}`);\n break;\n }\n case 'command-result': {\n _releaseGenerateSlot(msg.id);\n const pending = _pendingCommands.get(msg.id);\n if (!pending) break;\n _pendingCommands.delete(msg.id);\n if (msg.error !== undefined) pending.reject(new Error(msg.error));\n else pending.resolve(msg.result);\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(\n request: AparteChatRequest,\n _config?: string | Record<string, string>,\n ctx?: { providerId: string; signal?: AbortSignal },\n ): Promise<AparteChatResponse> {\n const requestId = uuid();\n const options = {\n maxTokens: request.maxTokens,\n temperature: request.temperature,\n seed: request.seed,\n };\n const signal = ctx?.signal;\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 // ── Stop, from either side ───────────────────────────────────────────\n // The transport's `ctx.signal` (the user's Stop, which the provider contract\n // says a bridge MUST honour — this one read it nowhere) and the stream's own\n // `cancel()` say the same thing, and the worker hears it once. Before the\n // generate has been posted there is nothing to interrupt: the stream is\n // settled here, and the slot is released when its turn in the chain comes —\n // not earlier, or the next generate would start over the one still running.\n let posted = false;\n let stopped = false;\n const stop = (): void => {\n if (stopped) return;\n stopped = true;\n signal?.removeEventListener('abort', stop);\n const ctrl = _pendingGenerates.get(requestId);\n _pendingGenerates.delete(requestId);\n if (posted) {\n _getWorker().postMessage({ type: 'cancel', id: requestId });\n // The reply ends HERE, not when the worker gets round to answering:\n // a token already in flight was otherwise enqueued into a stream the\n // user had stopped. `done` and not an error — a stop is not a failure,\n // and it is what settles the non-streaming path too. The worker's own\n // `gen-done` still releases the queue slot: that happens before it\n // looks the controller up.\n try { ctrl?.enqueue({ type: 'done' as const }); ctrl?.close(); } catch { /* already closed */ }\n return;\n }\n if (!ctrl) return;\n try { ctrl.enqueue({ type: 'error' as const, message: 'Generation cancelled before it started' }); ctrl.close(); }\n catch { /* already closed */ }\n };\n const postGenerate = (): void => {\n if (stopped) { _releaseGenerateSlot(requestId); return; }\n posted = true;\n _getWorker().postMessage({\n type: 'generate',\n id: requestId,\n modelId: request.modelId,\n // The conversation as it is, parts included: which parts a model can take\n // is the runner's knowledge, not this thread's.\n messages: request.messages,\n options,\n ..._selection(request.modelId),\n });\n };\n\n let response: AparteChatResponse | Promise<string>;\n if (request.stream === false) {\n response = 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 } else {\n response = new ReadableStream({\n async start(controller) {\n _pendingGenerates.set(requestId, controller);\n await prevGenerate;\n postGenerate();\n },\n cancel() {\n // The reader is gone, so nothing may be enqueued for it again — and the\n // model actually STOPS (not just the read): the worker interrupts this\n // generate, and the slot is still released by the resulting\n // gen-done/gen-error, so a queued generate cannot start before that.\n _pendingGenerates.delete(requestId);\n stop();\n },\n });\n }\n\n // `start` has run by now, so the controller is registered and a stop settles it.\n if (signal?.aborted) stop();\n else signal?.addEventListener('abort', stop, { once: true });\n return response;\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 return new Promise<void>((resolve, reject) => {\n _pendingPrepares.set(requestId, { modelId, onProgress, resolve, reject });\n _getWorker().postMessage({ type: 'prepare', id: requestId, modelId, ..._selection(modelId) });\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';\nexport type {\n TransformersRunner,\n RunnerContext,\n RunnerGenerateInput,\n RunnerProgress,\n RunnerModule,\n CreateRunner,\n BuiltInRunner,\n TransformersModule,\n} from './runners/types.js';\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/**\n * Send a runner something that is not a generation — swap an adapter, warm a cache, ask\n * a capability — and get its answer. The name and payload are the runner's vocabulary\n * (the built-in runners answer none). Queued behind the generates in flight: the worker\n * holds one runner, and a command on it mid-stream would race the stream.\n */\nexport function runnerCommand(modelId: string, name: string, payload: unknown): Promise<unknown> {\n const requestId = uuid();\n _queuedModelIds.set(requestId, modelId);\n const previous = _generateChain;\n _generateChain = new Promise<void>((resolveSlot) => {\n _generateDoneResolvers.set(requestId, resolveSlot);\n });\n return new Promise<unknown>((resolve, reject) => {\n _pendingCommands.set(requestId, { resolve, reject });\n void previous.then(() => {\n _getWorker().postMessage({ type: 'command', id: requestId, modelId, name, payload, ..._selection(modelId) });\n });\n });\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 _releaseWorkerBlob();\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 for (const c of _pendingCommands.values()) c.reject(new Error('Worker terminated'));\n _pendingCommands.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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"}
@@ -21,7 +21,6 @@ type HFMessage = {
21
21
  role: 'user' | 'assistant' | 'system';
22
22
  content: HFPart[];
23
23
  };
24
- export declare const UNSUPPORTED_PARTS_DROPPED = "Dropped content part(s) this vision runner cannot carry (only text and image parts reach the model).";
25
24
  /**
26
25
  * The conversation in the HF chat shape the processor's template expects — every turn's
27
26
  * content as parts, an `{ type: 'image' }` placeholder where a picture goes — plus the
@@ -1 +1 @@
1
- {"version":3,"file":"image-text-to-text.d.ts","sourceRoot":"","sources":["../../src/runners/image-text-to-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,YAAY,CAAC;AAGpE,KAAK,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjE,KAAK,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE9E,eAAO,MAAM,yBAAyB,yGACoE,CAAC;AAE3G;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG;IAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAsBtI;AAcD,eAAO,MAAM,YAAY,EAAE,YAsC1B,CAAC"}
1
+ {"version":3,"file":"image-text-to-text.d.ts","sourceRoot":"","sources":["../../src/runners/image-text-to-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,YAAY,CAAC;AAGpE,KAAK,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjE,KAAK,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE9E;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG;IAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA6BtI;AAcD,eAAO,MAAM,YAAY,EAAE,YAsC1B,CAAC"}
@@ -1,5 +1,4 @@
1
- import { l as loadOptions, i as interruptOn, t as textStreamer, g as generationOptions, T as TOOL_TURNS_DROPPED } from "./shared.js";
2
- const UNSUPPORTED_PARTS_DROPPED = "Dropped content part(s) this vision runner cannot carry (only text and image parts reach the model).";
1
+ import { l as loadOptions, i as interruptOn, t as textStreamer, g as generationOptions, T as TOOL_TURNS_DROPPED, U as UNSUPPORTED_PARTS_DROPPED } from "./shared.js";
3
2
  function toChatTemplate(messages, warn) {
4
3
  const chat = [];
5
4
  const images = [];
@@ -10,6 +9,7 @@ function toChatTemplate(messages, warn) {
10
9
  droppedToolTurns++;
11
10
  continue;
12
11
  }
12
+ if (m.toolCalls?.length) droppedToolTurns++;
13
13
  const parts = [];
14
14
  if (typeof m.content === "string") {
15
15
  if (m.content) parts.push({ type: "text", text: m.content });
@@ -58,7 +58,6 @@ const createRunner = async (ctx) => {
58
58
  };
59
59
  };
60
60
  export {
61
- UNSUPPORTED_PARTS_DROPPED,
62
61
  createRunner,
63
62
  toChatTemplate
64
63
  };
@@ -1 +1 @@
1
- {"version":3,"file":"image-text-to-text.js","sources":["../../src/runners/image-text-to-text.ts"],"sourcesContent":["/**\n * The built-in vision runner — a model that reads images and text and writes text\n * (SmolVLM, Qwen2-VL, LFM2-VL, Gemma 3, LLaVA…: everything `AutoModelForImageTextToText`\n * resolves).\n *\n * Transformers.js 4.x has no `image-text-to-text` PIPELINE, so this runner goes through\n * the model classes themselves, the way the SmolVLM examples do: `AutoProcessor` renders\n * the chat template with `{ type: 'image' }` placeholders, the images are decoded beside\n * the prompt in the same order, the processor turns both into tensors, and `generate()`\n * streams through a `TextStreamer`. Tool turns are dropped with the shared warning.\n */\n\nimport type { AparteChatMessage } from '@aparte/core';\nimport type { CreateRunner, RunnerGenerateInput } from './types.js';\nimport { TOOL_TURNS_DROPPED, generationOptions, interruptOn, loadOptions, textStreamer } from './shared.js';\n\ntype HFPart = { type: 'image' } | { type: 'text'; text: string };\ntype HFMessage = { role: 'user' | 'assistant' | 'system'; content: HFPart[] };\n\nexport const UNSUPPORTED_PARTS_DROPPED =\n 'Dropped content part(s) this vision runner cannot carry (only text and image parts reach the model).';\n\n/**\n * The conversation in the HF chat shape the processor's template expects — every turn's\n * content as parts, an `{ type: 'image' }` placeholder where a picture goes — plus the\n * pictures themselves, in order of appearance, for the processor to pair with them.\n */\nexport function toChatTemplate(messages: AparteChatMessage[], warn: (message: string) => void): { chat: HFMessage[]; images: string[] } {\n const chat: HFMessage[] = [];\n const images: string[] = [];\n let droppedToolTurns = 0;\n let droppedParts = 0;\n for (const m of messages) {\n if (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'system') { droppedToolTurns++; continue; }\n const parts: HFPart[] = [];\n if (typeof m.content === 'string') {\n if (m.content) parts.push({ type: 'text', text: m.content });\n } else {\n for (const p of m.content) {\n if (p.type === 'text') { if (p.text) parts.push({ type: 'text', text: p.text }); }\n else if (p.type === 'image') { images.push(p.image); parts.push({ type: 'image' }); }\n else droppedParts++;\n }\n }\n if (parts.length > 0) chat.push({ role: m.role, content: parts });\n }\n if (droppedToolTurns > 0) warn(TOOL_TURNS_DROPPED);\n if (droppedParts > 0) warn(UNSUPPORTED_PARTS_DROPPED);\n return { chat, images };\n}\n\n/** The processor and model, as this runner calls them (Transformers.js types them through `Callable`). */\ninterface VisionProcessor {\n (text: string, images: unknown[]): Promise<Record<string, unknown>>;\n /** The text half alone — callable, for a turn that carries no picture. */\n tokenizer: (text: string) => Record<string, unknown> | Promise<Record<string, unknown>>;\n apply_chat_template(messages: HFMessage[], options: Record<string, unknown>): unknown;\n}\ninterface VisionModel {\n generate(args: Record<string, unknown>): Promise<unknown>;\n dispose(): Promise<unknown>;\n}\n\nexport const createRunner: CreateRunner = async (ctx) => {\n const tf = ctx.transformers as unknown as {\n AutoProcessor: { from_pretrained(model: string, options?: unknown): Promise<VisionProcessor> };\n AutoModelForImageTextToText: { from_pretrained(model: string, options?: unknown): Promise<VisionModel> };\n load_image(source: string): Promise<unknown>;\n };\n // The processor carries no weights: dtype and device are the model's alone.\n const [processor, model] = await Promise.all([\n tf.AutoProcessor.from_pretrained(ctx.modelId),\n tf.AutoModelForImageTextToText.from_pretrained(ctx.modelId, loadOptions(ctx)),\n ]);\n\n return {\n async generate({ messages, options, emit, signal }: RunnerGenerateInput): Promise<void> {\n const { stopping, release } = interruptOn(signal, ctx.transformers);\n try {\n const { chat, images } = toChatTemplate(messages, ctx.warn);\n const prompt = processor.apply_chat_template(chat, { add_generation_prompt: true, tokenize: false }) as string;\n // The processor pairs the prompt with pictures and wants at least one (Idefics3's\n // reads `images.rows` — \"hello\" as a first message crashed it on a real SmolVLM).\n // A turn with no image is text: the tokenizer alone takes it.\n const inputs = images.length > 0\n ? await processor(prompt, await Promise.all(images.map((src) => tf.load_image(src))))\n : await processor.tokenizer(prompt);\n await model.generate({\n ...inputs,\n ...generationOptions(options),\n streamer: textStreamer(ctx.transformers, processor.tokenizer, emit),\n stopping_criteria: stopping,\n });\n } finally {\n release();\n }\n },\n async dispose() {\n await model.dispose();\n },\n };\n};\n"],"names":[],"mappings":";AAmBO,MAAM,4BACT;AAOG,SAAS,eAAe,UAA+B,MAA0E;AACpI,QAAM,OAAoB,CAAA;AAC1B,QAAM,SAAmB,CAAA;AACzB,MAAI,mBAAmB;AACvB,MAAI,eAAe;AACnB,aAAW,KAAK,UAAU;AACtB,QAAI,EAAE,SAAS,UAAU,EAAE,SAAS,eAAe,EAAE,SAAS,UAAU;AAAE;AAAoB;AAAA,IAAU;AACxG,UAAM,QAAkB,CAAA;AACxB,QAAI,OAAO,EAAE,YAAY,UAAU;AAC/B,UAAI,EAAE,QAAS,OAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,EAAE,SAAS;AAAA,IAC/D,OAAO;AACH,iBAAW,KAAK,EAAE,SAAS;AACvB,YAAI,EAAE,SAAS,QAAQ;AAAE,cAAI,EAAE,KAAM,OAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,EAAE,MAAM;AAAA,QAAG,WACxE,EAAE,SAAS,SAAS;AAAE,iBAAO,KAAK,EAAE,KAAK;AAAG,gBAAM,KAAK,EAAE,MAAM,QAAA,CAAS;AAAA,QAAG,MAC/E;AAAA,MACT;AAAA,IACJ;AACA,QAAI,MAAM,SAAS,EAAG,MAAK,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,MAAA,CAAO;AAAA,EACpE;AACA,MAAI,mBAAmB,EAAG,MAAK,kBAAkB;AACjD,MAAI,eAAe,EAAG,MAAK,yBAAyB;AACpD,SAAO,EAAE,MAAM,OAAA;AACnB;AAcO,MAAM,eAA6B,OAAO,QAAQ;AACrD,QAAM,KAAK,IAAI;AAMf,QAAM,CAAC,WAAW,KAAK,IAAI,MAAM,QAAQ,IAAI;AAAA,IACzC,GAAG,cAAc,gBAAgB,IAAI,OAAO;AAAA,IAC5C,GAAG,4BAA4B,gBAAgB,IAAI,SAAS,YAAY,GAAG,CAAC;AAAA,EAAA,CAC/E;AAED,SAAO;AAAA,IACH,MAAM,SAAS,EAAE,UAAU,SAAS,MAAM,UAA8C;AACpF,YAAM,EAAE,UAAU,QAAA,IAAY,YAAY,QAAQ,IAAI,YAAY;AAClE,UAAI;AACA,cAAM,EAAE,MAAM,OAAA,IAAW,eAAe,UAAU,IAAI,IAAI;AAC1D,cAAM,SAAS,UAAU,oBAAoB,MAAM,EAAE,uBAAuB,MAAM,UAAU,OAAO;AAInG,cAAM,SAAS,OAAO,SAAS,IACzB,MAAM,UAAU,QAAQ,MAAM,QAAQ,IAAI,OAAO,IAAI,CAAC,QAAQ,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,IAClF,MAAM,UAAU,UAAU,MAAM;AACtC,cAAM,MAAM,SAAS;AAAA,UACjB,GAAG;AAAA,UACH,GAAG,kBAAkB,OAAO;AAAA,UAC5B,UAAU,aAAa,IAAI,cAAc,UAAU,WAAW,IAAI;AAAA,UAClE,mBAAmB;AAAA,QAAA,CACtB;AAAA,MACL,UAAA;AACI,gBAAA;AAAA,MACJ;AAAA,IACJ;AAAA,IACA,MAAM,UAAU;AACZ,YAAM,MAAM,QAAA;AAAA,IAChB;AAAA,EAAA;AAER;"}
1
+ {"version":3,"file":"image-text-to-text.js","sources":["../../src/runners/image-text-to-text.ts"],"sourcesContent":["/**\n * The built-in vision runner — a model that reads images and text and writes text\n * (SmolVLM, Qwen2-VL, LFM2-VL, Gemma 3, LLaVA…: everything `AutoModelForImageTextToText`\n * resolves).\n *\n * Transformers.js 4.x has no `image-text-to-text` PIPELINE, so this runner goes through\n * the model classes themselves, the way the SmolVLM examples do: `AutoProcessor` renders\n * the chat template with `{ type: 'image' }` placeholders, the images are decoded beside\n * the prompt in the same order, the processor turns both into tensors, and `generate()`\n * streams through a `TextStreamer`. Tool turns are dropped with the shared warning.\n */\n\nimport type { AparteChatMessage } from '@aparte/core';\nimport type { CreateRunner, RunnerGenerateInput } from './types.js';\nimport { TOOL_TURNS_DROPPED, UNSUPPORTED_PARTS_DROPPED, generationOptions, interruptOn, loadOptions, textStreamer } from './shared.js';\n\ntype HFPart = { type: 'image' } | { type: 'text'; text: string };\ntype HFMessage = { role: 'user' | 'assistant' | 'system'; content: HFPart[] };\n\n/**\n * The conversation in the HF chat shape the processor's template expects — every turn's\n * content as parts, an `{ type: 'image' }` placeholder where a picture goes — plus the\n * pictures themselves, in order of appearance, for the processor to pair with them.\n */\nexport function toChatTemplate(messages: AparteChatMessage[], warn: (message: string) => void): { chat: HFMessage[]; images: string[] } {\n const chat: HFMessage[] = [];\n const images: string[] = [];\n let droppedToolTurns = 0;\n let droppedParts = 0;\n for (const m of messages) {\n if (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'system') { droppedToolTurns++; continue; }\n // An assistant's calls ride on an `assistant` message, which passes the role test\n // above and, when the model said nothing before them, carries no part either — so\n // without this the whole turn leaves the prompt with nothing said.\n if (m.toolCalls?.length) droppedToolTurns++;\n const parts: HFPart[] = [];\n if (typeof m.content === 'string') {\n if (m.content) parts.push({ type: 'text', text: m.content });\n } else {\n for (const p of m.content) {\n if (p.type === 'text') { if (p.text) parts.push({ type: 'text', text: p.text }); }\n else if (p.type === 'image') { images.push(p.image); parts.push({ type: 'image' }); }\n // Not an arm the union can produce, and the mirror of the role-axis guard the\n // wire mappers carry: an app built against an older aparte can still hand us a\n // `file` part, and pushing its absent `image` gave `load_image(undefined)`.\n else droppedParts++;\n }\n }\n if (parts.length > 0) chat.push({ role: m.role, content: parts });\n }\n if (droppedToolTurns > 0) warn(TOOL_TURNS_DROPPED);\n if (droppedParts > 0) warn(UNSUPPORTED_PARTS_DROPPED);\n return { chat, images };\n}\n\n/** The processor and model, as this runner calls them (Transformers.js types them through `Callable`). */\ninterface VisionProcessor {\n (text: string, images: unknown[]): Promise<Record<string, unknown>>;\n /** The text half alone — callable, for a turn that carries no picture. */\n tokenizer: (text: string) => Record<string, unknown> | Promise<Record<string, unknown>>;\n apply_chat_template(messages: HFMessage[], options: Record<string, unknown>): unknown;\n}\ninterface VisionModel {\n generate(args: Record<string, unknown>): Promise<unknown>;\n dispose(): Promise<unknown>;\n}\n\nexport const createRunner: CreateRunner = async (ctx) => {\n const tf = ctx.transformers as unknown as {\n AutoProcessor: { from_pretrained(model: string, options?: unknown): Promise<VisionProcessor> };\n AutoModelForImageTextToText: { from_pretrained(model: string, options?: unknown): Promise<VisionModel> };\n load_image(source: string): Promise<unknown>;\n };\n // The processor carries no weights: dtype and device are the model's alone.\n const [processor, model] = await Promise.all([\n tf.AutoProcessor.from_pretrained(ctx.modelId),\n tf.AutoModelForImageTextToText.from_pretrained(ctx.modelId, loadOptions(ctx)),\n ]);\n\n return {\n async generate({ messages, options, emit, signal }: RunnerGenerateInput): Promise<void> {\n const { stopping, release } = interruptOn(signal, ctx.transformers);\n try {\n const { chat, images } = toChatTemplate(messages, ctx.warn);\n const prompt = processor.apply_chat_template(chat, { add_generation_prompt: true, tokenize: false }) as string;\n // The processor pairs the prompt with pictures and wants at least one (Idefics3's\n // reads `images.rows` — \"hello\" as a first message crashed it on a real SmolVLM).\n // A turn with no image is text: the tokenizer alone takes it.\n const inputs = images.length > 0\n ? await processor(prompt, await Promise.all(images.map((src) => tf.load_image(src))))\n : await processor.tokenizer(prompt);\n await model.generate({\n ...inputs,\n ...generationOptions(options),\n streamer: textStreamer(ctx.transformers, processor.tokenizer, emit),\n stopping_criteria: stopping,\n });\n } finally {\n release();\n }\n },\n async dispose() {\n await model.dispose();\n },\n };\n};\n"],"names":[],"mappings":";AAwBO,SAAS,eAAe,UAA+B,MAA0E;AACpI,QAAM,OAAoB,CAAA;AAC1B,QAAM,SAAmB,CAAA;AACzB,MAAI,mBAAmB;AACvB,MAAI,eAAe;AACnB,aAAW,KAAK,UAAU;AACtB,QAAI,EAAE,SAAS,UAAU,EAAE,SAAS,eAAe,EAAE,SAAS,UAAU;AAAE;AAAoB;AAAA,IAAU;AAIxG,QAAI,EAAE,WAAW,OAAQ;AACzB,UAAM,QAAkB,CAAA;AACxB,QAAI,OAAO,EAAE,YAAY,UAAU;AAC/B,UAAI,EAAE,QAAS,OAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,EAAE,SAAS;AAAA,IAC/D,OAAO;AACH,iBAAW,KAAK,EAAE,SAAS;AACvB,YAAI,EAAE,SAAS,QAAQ;AAAE,cAAI,EAAE,KAAM,OAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,EAAE,MAAM;AAAA,QAAG,WACxE,EAAE,SAAS,SAAS;AAAE,iBAAO,KAAK,EAAE,KAAK;AAAG,gBAAM,KAAK,EAAE,MAAM,QAAA,CAAS;AAAA,QAAG,MAI/E;AAAA,MACT;AAAA,IACJ;AACA,QAAI,MAAM,SAAS,EAAG,MAAK,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,MAAA,CAAO;AAAA,EACpE;AACA,MAAI,mBAAmB,EAAG,MAAK,kBAAkB;AACjD,MAAI,eAAe,EAAG,MAAK,yBAAyB;AACpD,SAAO,EAAE,MAAM,OAAA;AACnB;AAcO,MAAM,eAA6B,OAAO,QAAQ;AACrD,QAAM,KAAK,IAAI;AAMf,QAAM,CAAC,WAAW,KAAK,IAAI,MAAM,QAAQ,IAAI;AAAA,IACzC,GAAG,cAAc,gBAAgB,IAAI,OAAO;AAAA,IAC5C,GAAG,4BAA4B,gBAAgB,IAAI,SAAS,YAAY,GAAG,CAAC;AAAA,EAAA,CAC/E;AAED,SAAO;AAAA,IACH,MAAM,SAAS,EAAE,UAAU,SAAS,MAAM,UAA8C;AACpF,YAAM,EAAE,UAAU,QAAA,IAAY,YAAY,QAAQ,IAAI,YAAY;AAClE,UAAI;AACA,cAAM,EAAE,MAAM,OAAA,IAAW,eAAe,UAAU,IAAI,IAAI;AAC1D,cAAM,SAAS,UAAU,oBAAoB,MAAM,EAAE,uBAAuB,MAAM,UAAU,OAAO;AAInG,cAAM,SAAS,OAAO,SAAS,IACzB,MAAM,UAAU,QAAQ,MAAM,QAAQ,IAAI,OAAO,IAAI,CAAC,QAAQ,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,IAClF,MAAM,UAAU,UAAU,MAAM;AACtC,cAAM,MAAM,SAAS;AAAA,UACjB,GAAG;AAAA,UACH,GAAG,kBAAkB,OAAO;AAAA,UAC5B,UAAU,aAAa,IAAI,cAAc,UAAU,WAAW,IAAI;AAAA,UAClE,mBAAmB;AAAA,QAAA,CACtB;AAAA,MACL,UAAA;AACI,gBAAA;AAAA,MACJ;AAAA,IACJ;AAAA,IACA,MAAM,UAAU;AACZ,YAAM,MAAM,QAAA;AAAA,IAChB;AAAA,EAAA;AAER;"}
@@ -6,6 +6,15 @@
6
6
  import type { AparteStreamEvent } from '@aparte/core';
7
7
  import type { RunnerContext, TransformersModule } from './types.js';
8
8
  export declare const TOOL_TURNS_DROPPED: string;
9
+ /**
10
+ * A content part neither built-in runner can carry. The union is text and image, so this
11
+ * is unreachable from typed code — and reachable all the same from an app built against an
12
+ * older aparte, which declared a third `file` part nothing ever filled. The wire mappers
13
+ * guard the same case on the ROLE axis (`openai-compat`'s removed `tool_call`/`tool_result`);
14
+ * the part axis had no guard, so the part was pushed into the image list as `undefined` and
15
+ * `load_image` threw at generate time instead of the part being counted and named.
16
+ */
17
+ export declare const UNSUPPORTED_PARTS_DROPPED: string;
9
18
  /**
10
19
  * The options a `from_pretrained` / `pipeline()` call takes from the context: download
11
20
  * progress forwarded to the page (percentages, rounded), dtype and device when set.
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/runners/shared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEpE,eAAO,MAAM,kBAAkB,QAGO,CAAC;AAEvC;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUvE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,OAAO,IAAI,IAAI,CAAA;CAAE,CAMzH;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,YAAY,EAAE,kBAAkB,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAOpI;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOhH"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/runners/shared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEpE,eAAO,MAAM,kBAAkB,QAI6C,CAAC;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAIJ,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUvE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,OAAO,IAAI,IAAI,CAAA;CAAE,CAMzH;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,YAAY,EAAE,kBAAkB,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAOpI;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOhH"}
@@ -1,4 +1,5 @@
1
- const TOOL_TURNS_DROPPED = "Dropped tool turn(s) from the prompt: this runner does not support tool calling, so the model will not see the call or its result. Use an OpenAI-compatible endpoint for tools, or a runner that renders them.";
1
+ const TOOL_TURNS_DROPPED = "Dropped the tool call(s) and their results from the prompt: this runner does not support tool calling, so the model sees neither the call the assistant made nor the result that answered it. What the assistant SAID before calling stays in the prompt. Use an OpenAI-compatible endpoint for tools, or a runner that renders them.";
2
+ const UNSUPPORTED_PARTS_DROPPED = "Dropped content part(s) this runner cannot carry: it takes text and image parts only, so anything else leaves the prompt with nothing in its place. A `file` part is the case to expect — it was removed from `AparteContentPart`; inline what you want the model to read as text, or send an image.";
2
3
  function loadOptions(ctx) {
3
4
  const opts = {
4
5
  progress_callback: (p) => {
@@ -41,6 +42,7 @@ function generationOptions(options) {
41
42
  }
42
43
  export {
43
44
  TOOL_TURNS_DROPPED as T,
45
+ UNSUPPORTED_PARTS_DROPPED as U,
44
46
  generationOptions as g,
45
47
  interruptOn as i,
46
48
  loadOptions as l,
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","sources":["../../src/runners/shared.ts"],"sourcesContent":["/**\n * What the two built-in runners share — kept in one place so the two cannot drift on\n * how progress is reported, how a stop reaches the model, or what a dropped tool turn\n * says. Types only from core (see `text-generation.ts` for why).\n */\n\nimport type { AparteStreamEvent } from '@aparte/core';\nimport type { RunnerContext, TransformersModule } from './types.js';\n\nexport const TOOL_TURNS_DROPPED =\n 'Dropped tool turn(s) from the prompt: this runner does not support tool calling, so the '\n + 'model will not see the call or its result. Use an OpenAI-compatible endpoint for tools, '\n + 'or a runner that renders them.';\n\n/**\n * The options a `from_pretrained` / `pipeline()` call takes from the context: download\n * progress forwarded to the page (percentages, rounded), dtype and device when set.\n */\nexport function loadOptions(ctx: RunnerContext): Record<string, unknown> {\n const opts: Record<string, unknown> = {\n progress_callback: (p: { status?: string; file?: string; progress?: number }) => {\n if (p.status === 'progress') ctx.progress({ status: 'downloading', file: p.file, progress: Math.round(p.progress ?? 0) });\n else if (p.status === 'done') ctx.progress({ status: 'loading', file: p.file });\n },\n };\n if (ctx.dtype) opts['dtype'] = ctx.dtype;\n if (ctx.device && ctx.device !== 'auto') opts['device'] = ctx.device;\n return opts;\n}\n\n/**\n * A stopping criteria the signal interrupts — so a Stop actually STOPS the model, not just\n * the read; otherwise generation runs to `max_new_tokens` off-thread, spending exactly the\n * CPU/GPU/battery this provider exists to save. Call `release()` in a `finally`.\n */\nexport function interruptOn(signal: AbortSignal, transformers: TransformersModule): { stopping: unknown; release(): void } {\n const stopping = new transformers.InterruptableStoppingCriteria();\n const onAbort = (): void => { stopping.interrupt(); };\n if (signal.aborted) onAbort();\n else signal.addEventListener('abort', onAbort, { once: true });\n return { stopping, release: () => { signal.removeEventListener('abort', onAbort); } };\n}\n\n/** A `TextStreamer` that emits each decoded token as a `text` event, prompt skipped. */\nexport function textStreamer(transformers: TransformersModule, tokenizer: unknown, emit: (event: AparteStreamEvent) => void): unknown {\n const TextStreamer = transformers.TextStreamer as unknown as new (tokenizer: unknown, options: Record<string, unknown>) => unknown;\n return new TextStreamer(tokenizer, {\n skip_prompt: true,\n skip_special_tokens: true,\n callback_function: (text: string) => { if (text) emit({ type: 'text', delta: text }); },\n });\n}\n\n/** Sampling options in Transformers.js' vocabulary, from the request's. */\nexport function generationOptions(options: { maxTokens?: number; temperature?: number }): Record<string, unknown> {\n const temperature = options.temperature ?? 0;\n return {\n max_new_tokens: options.maxTokens ?? 512,\n do_sample: temperature > 0,\n temperature: temperature > 0 ? temperature : undefined,\n };\n}\n"],"names":[],"mappings":"AASO,MAAM,qBACT;AAQG,SAAS,YAAY,KAA6C;AACrE,QAAM,OAAgC;AAAA,IAClC,mBAAmB,CAAC,MAA6D;AAC7E,UAAI,EAAE,WAAW,gBAAgB,SAAS,EAAE,QAAQ,eAAe,MAAM,EAAE,MAAM,UAAU,KAAK,MAAM,EAAE,YAAY,CAAC,GAAG;AAAA,eAC/G,EAAE,WAAW,OAAQ,KAAI,SAAS,EAAE,QAAQ,WAAW,MAAM,EAAE,KAAA,CAAM;AAAA,IAClF;AAAA,EAAA;AAEJ,MAAI,IAAI,MAAO,MAAK,OAAO,IAAI,IAAI;AACnC,MAAI,IAAI,UAAU,IAAI,WAAW,OAAQ,MAAK,QAAQ,IAAI,IAAI;AAC9D,SAAO;AACX;AAOO,SAAS,YAAY,QAAqB,cAA0E;AACvH,QAAM,WAAW,IAAI,aAAa,8BAAA;AAClC,QAAM,UAAU,MAAY;AAAE,aAAS,UAAA;AAAA,EAAa;AACpD,MAAI,OAAO,QAAS,SAAA;AAAA,cACR,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM;AAC7D,SAAO,EAAE,UAAU,SAAS,MAAM;AAAE,WAAO,oBAAoB,SAAS,OAAO;AAAA,EAAG,EAAA;AACtF;AAGO,SAAS,aAAa,cAAkC,WAAoB,MAAmD;AAClI,QAAM,eAAe,aAAa;AAClC,SAAO,IAAI,aAAa,WAAW;AAAA,IAC/B,aAAa;AAAA,IACb,qBAAqB;AAAA,IACrB,mBAAmB,CAAC,SAAiB;AAAE,UAAI,KAAM,MAAK,EAAE,MAAM,QAAQ,OAAO,MAAM;AAAA,IAAG;AAAA,EAAA,CACzF;AACL;AAGO,SAAS,kBAAkB,SAAgF;AAC9G,QAAM,cAAc,QAAQ,eAAe;AAC3C,SAAO;AAAA,IACH,gBAAgB,QAAQ,aAAa;AAAA,IACrC,WAAW,cAAc;AAAA,IACzB,aAAa,cAAc,IAAI,cAAc;AAAA,EAAA;AAErD;"}
1
+ {"version":3,"file":"shared.js","sources":["../../src/runners/shared.ts"],"sourcesContent":["/**\n * What the two built-in runners share — kept in one place so the two cannot drift on\n * how progress is reported, how a stop reaches the model, or what a dropped tool turn\n * says. Types only from core (see `text-generation.ts` for why).\n */\n\nimport type { AparteStreamEvent } from '@aparte/core';\nimport type { RunnerContext, TransformersModule } from './types.js';\n\nexport const TOOL_TURNS_DROPPED =\n 'Dropped the tool call(s) and their results from the prompt: this runner does not support '\n + 'tool calling, so the model sees neither the call the assistant made nor the result that '\n + 'answered it. What the assistant SAID before calling stays in the prompt. Use an '\n + 'OpenAI-compatible endpoint for tools, or a runner that renders them.';\n\n/**\n * A content part neither built-in runner can carry. The union is text and image, so this\n * is unreachable from typed code — and reachable all the same from an app built against an\n * older aparte, which declared a third `file` part nothing ever filled. The wire mappers\n * guard the same case on the ROLE axis (`openai-compat`'s removed `tool_call`/`tool_result`);\n * the part axis had no guard, so the part was pushed into the image list as `undefined` and\n * `load_image` threw at generate time instead of the part being counted and named.\n */\nexport const UNSUPPORTED_PARTS_DROPPED =\n 'Dropped content part(s) this runner cannot carry: it takes text and image parts only, so '\n + 'anything else leaves the prompt with nothing in its place. A `file` part is the case to '\n + 'expect — it was removed from `AparteContentPart`; inline what you want the model to read '\n + 'as text, or send an image.';\n\n/**\n * The options a `from_pretrained` / `pipeline()` call takes from the context: download\n * progress forwarded to the page (percentages, rounded), dtype and device when set.\n */\nexport function loadOptions(ctx: RunnerContext): Record<string, unknown> {\n const opts: Record<string, unknown> = {\n progress_callback: (p: { status?: string; file?: string; progress?: number }) => {\n if (p.status === 'progress') ctx.progress({ status: 'downloading', file: p.file, progress: Math.round(p.progress ?? 0) });\n else if (p.status === 'done') ctx.progress({ status: 'loading', file: p.file });\n },\n };\n if (ctx.dtype) opts['dtype'] = ctx.dtype;\n if (ctx.device && ctx.device !== 'auto') opts['device'] = ctx.device;\n return opts;\n}\n\n/**\n * A stopping criteria the signal interrupts — so a Stop actually STOPS the model, not just\n * the read; otherwise generation runs to `max_new_tokens` off-thread, spending exactly the\n * CPU/GPU/battery this provider exists to save. Call `release()` in a `finally`.\n */\nexport function interruptOn(signal: AbortSignal, transformers: TransformersModule): { stopping: unknown; release(): void } {\n const stopping = new transformers.InterruptableStoppingCriteria();\n const onAbort = (): void => { stopping.interrupt(); };\n if (signal.aborted) onAbort();\n else signal.addEventListener('abort', onAbort, { once: true });\n return { stopping, release: () => { signal.removeEventListener('abort', onAbort); } };\n}\n\n/** A `TextStreamer` that emits each decoded token as a `text` event, prompt skipped. */\nexport function textStreamer(transformers: TransformersModule, tokenizer: unknown, emit: (event: AparteStreamEvent) => void): unknown {\n const TextStreamer = transformers.TextStreamer as unknown as new (tokenizer: unknown, options: Record<string, unknown>) => unknown;\n return new TextStreamer(tokenizer, {\n skip_prompt: true,\n skip_special_tokens: true,\n callback_function: (text: string) => { if (text) emit({ type: 'text', delta: text }); },\n });\n}\n\n/** Sampling options in Transformers.js' vocabulary, from the request's. */\nexport function generationOptions(options: { maxTokens?: number; temperature?: number }): Record<string, unknown> {\n const temperature = options.temperature ?? 0;\n return {\n max_new_tokens: options.maxTokens ?? 512,\n do_sample: temperature > 0,\n temperature: temperature > 0 ? temperature : undefined,\n };\n}\n"],"names":[],"mappings":"AASO,MAAM,qBACT;AAaG,MAAM,4BACT;AASG,SAAS,YAAY,KAA6C;AACrE,QAAM,OAAgC;AAAA,IAClC,mBAAmB,CAAC,MAA6D;AAC7E,UAAI,EAAE,WAAW,gBAAgB,SAAS,EAAE,QAAQ,eAAe,MAAM,EAAE,MAAM,UAAU,KAAK,MAAM,EAAE,YAAY,CAAC,GAAG;AAAA,eAC/G,EAAE,WAAW,OAAQ,KAAI,SAAS,EAAE,QAAQ,WAAW,MAAM,EAAE,KAAA,CAAM;AAAA,IAClF;AAAA,EAAA;AAEJ,MAAI,IAAI,MAAO,MAAK,OAAO,IAAI,IAAI;AACnC,MAAI,IAAI,UAAU,IAAI,WAAW,OAAQ,MAAK,QAAQ,IAAI,IAAI;AAC9D,SAAO;AACX;AAOO,SAAS,YAAY,QAAqB,cAA0E;AACvH,QAAM,WAAW,IAAI,aAAa,8BAAA;AAClC,QAAM,UAAU,MAAY;AAAE,aAAS,UAAA;AAAA,EAAa;AACpD,MAAI,OAAO,QAAS,SAAA;AAAA,cACR,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM;AAC7D,SAAO,EAAE,UAAU,SAAS,MAAM;AAAE,WAAO,oBAAoB,SAAS,OAAO;AAAA,EAAG,EAAA;AACtF;AAGO,SAAS,aAAa,cAAkC,WAAoB,MAAmD;AAClI,QAAM,eAAe,aAAa;AAClC,SAAO,IAAI,aAAa,WAAW;AAAA,IAC/B,aAAa;AAAA,IACb,qBAAqB;AAAA,IACrB,mBAAmB,CAAC,SAAiB;AAAE,UAAI,KAAM,MAAK,EAAE,MAAM,QAAQ,OAAO,MAAM;AAAA,IAAG;AAAA,EAAA,CACzF;AACL;AAGO,SAAS,kBAAkB,SAAgF;AAC9G,QAAM,cAAc,QAAQ,eAAe;AAC3C,SAAO;AAAA,IACH,gBAAgB,QAAQ,aAAa;AAAA,IACrC,WAAW,cAAc;AAAA,IACzB,aAAa,cAAc,IAAI,cAAc;AAAA,EAAA;AAErD;"}
@@ -1 +1 @@
1
- {"version":3,"file":"text-generation.d.ts","sourceRoot":"","sources":["../../src/runners/text-generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAuB,MAAM,YAAY,CAAC;AAGnF,KAAK,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhF,eAAO,MAAM,cAAc,QAGoB,CAAC;AAWhD,sEAAsE;AACtE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,aAAa,EAAE,CAgBlH;AASD,eAAO,MAAM,YAAY,EAAE,YAqB1B,CAAC"}
1
+ {"version":3,"file":"text-generation.d.ts","sourceRoot":"","sources":["../../src/runners/text-generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAuB,MAAM,YAAY,CAAC;AAGnF,KAAK,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhF,eAAO,MAAM,cAAc,QAGoB,CAAC;AAWhD,sEAAsE;AACtE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,aAAa,EAAE,CAoBlH;AASD,eAAO,MAAM,YAAY,EAAE,YAqB1B,CAAC"}
@@ -10,6 +10,7 @@ function flattenForChatTemplate(messages, warn) {
10
10
  let droppedToolTurns = 0;
11
11
  for (const m of messages) {
12
12
  if (m.role === "user" || m.role === "assistant" || m.role === "system") {
13
+ if (m.toolCalls?.length) droppedToolTurns++;
13
14
  if (Array.isArray(m.content)) droppedImages += m.content.filter((p) => p.type === "image").length;
14
15
  const text = textOf(m.content);
15
16
  if (text) result.push({ role: m.role, content: text });
@@ -1 +1 @@
1
- {"version":3,"file":"text-generation.js","sources":["../../src/runners/text-generation.ts"],"sourcesContent":["/**\n * The built-in text runner — the generic `pipeline('text-generation')` path this\n * provider has always run, extracted from the worker so it is one runner among others.\n *\n * It flattens the conversation to `{ role, content: string }` turns (the tokenizer applies\n * the chat template). Two things it cannot carry, it SAYS: tool turns (their wire syntax is\n * model-specific) and image parts (a text model has no eyes). The second used to vanish\n * silently — a photo attached to a text model produced an answer that pretended — and\n * that silence, not the limitation, was the defect.\n */\n\n// Types only. A runner runs INSIDE the worker, and the worker bundle has no way to\n// resolve `@aparte/core` at runtime (an import map does not reach a worker) — so a value\n// import here is not externalised, it is inlined: the first build that imported\n// `contentToText` shipped all of core, components included, in a 426 kB runner chunk.\nimport type { AparteChatMessage, AparteContentPart } from '@aparte/core';\nimport type { CreateRunner, RunnerContext, RunnerGenerateInput } from './types.js';\nimport { TOOL_TURNS_DROPPED, generationOptions, interruptOn, loadOptions, textStreamer } from './shared.js';\n\ntype SimpleMessage = { role: 'user' | 'assistant' | 'system'; content: string };\n\nexport const IMAGES_DROPPED =\n 'This model has no vision runner: image parts were dropped from the prompt, so the model '\n + 'answers as if there were none. Register the model with task: \"image-text-to-text\", or '\n + 'point `runner` at a module of your own.';\n\n/** The text parts of a message, joined — what a text-only chat template can take. */\nfunction textOf(content: string | AparteContentPart[]): string {\n if (typeof content === 'string') return content;\n return content\n .filter((p): p is Extract<AparteContentPart, { type: 'text' }> => p.type === 'text')\n .map((p) => p.text)\n .join('');\n}\n\n/** Flatten to what the chat template takes; say what was left out. */\nexport function flattenForChatTemplate(messages: AparteChatMessage[], warn: RunnerContext['warn']): SimpleMessage[] {\n const result: SimpleMessage[] = [];\n let droppedImages = 0;\n let droppedToolTurns = 0;\n for (const m of messages) {\n if (m.role === 'user' || m.role === 'assistant' || m.role === 'system') {\n if (Array.isArray(m.content)) droppedImages += m.content.filter((p) => p.type === 'image').length;\n const text = textOf(m.content);\n if (text) result.push({ role: m.role, content: text });\n } else {\n droppedToolTurns++;\n }\n }\n if (droppedImages > 0) warn(IMAGES_DROPPED);\n if (droppedToolTurns > 0) warn(TOOL_TURNS_DROPPED);\n return result;\n}\n\n/** The pipeline, as this runner calls it. Transformers.js types it per task; this is the one task. */\ninterface TextPipeline {\n (messages: SimpleMessage[], options: Record<string, unknown>): Promise<unknown>;\n tokenizer: unknown;\n dispose?: () => Promise<void>;\n}\n\nexport const createRunner: CreateRunner = async (ctx) => {\n const pipeline = ctx.transformers.pipeline as unknown as (task: string, model: string, options: unknown) => Promise<TextPipeline>;\n const pipe = await pipeline('text-generation', ctx.modelId, loadOptions(ctx));\n\n return {\n async generate({ messages, options, emit, signal }: RunnerGenerateInput): Promise<void> {\n const { stopping, release } = interruptOn(signal, ctx.transformers);\n try {\n await pipe(flattenForChatTemplate(messages, ctx.warn), {\n ...generationOptions(options),\n streamer: textStreamer(ctx.transformers, pipe.tokenizer, emit),\n stopping_criteria: stopping,\n });\n } finally {\n release();\n }\n },\n dispose() {\n return pipe.dispose?.();\n },\n };\n};\n"],"names":[],"mappings":";AAqBO,MAAM,iBACT;AAKJ,SAAS,OAAO,SAA+C;AAC3D,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,SAAO,QACF,OAAO,CAAC,MAAyD,EAAE,SAAS,MAAM,EAClF,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,KAAK,EAAE;AAChB;AAGO,SAAS,uBAAuB,UAA+B,MAA8C;AAChH,QAAM,SAA0B,CAAA;AAChC,MAAI,gBAAgB;AACpB,MAAI,mBAAmB;AACvB,aAAW,KAAK,UAAU;AACtB,QAAI,EAAE,SAAS,UAAU,EAAE,SAAS,eAAe,EAAE,SAAS,UAAU;AACpE,UAAI,MAAM,QAAQ,EAAE,OAAO,EAAG,kBAAiB,EAAE,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE;AAC3F,YAAM,OAAO,OAAO,EAAE,OAAO;AAC7B,UAAI,aAAa,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,MAAM;AAAA,IACzD,OAAO;AACH;AAAA,IACJ;AAAA,EACJ;AACA,MAAI,gBAAgB,EAAG,MAAK,cAAc;AAC1C,MAAI,mBAAmB,EAAG,MAAK,kBAAkB;AACjD,SAAO;AACX;AASO,MAAM,eAA6B,OAAO,QAAQ;AACrD,QAAM,WAAW,IAAI,aAAa;AAClC,QAAM,OAAO,MAAM,SAAS,mBAAmB,IAAI,SAAS,YAAY,GAAG,CAAC;AAE5E,SAAO;AAAA,IACH,MAAM,SAAS,EAAE,UAAU,SAAS,MAAM,UAA8C;AACpF,YAAM,EAAE,UAAU,QAAA,IAAY,YAAY,QAAQ,IAAI,YAAY;AAClE,UAAI;AACA,cAAM,KAAK,uBAAuB,UAAU,IAAI,IAAI,GAAG;AAAA,UACnD,GAAG,kBAAkB,OAAO;AAAA,UAC5B,UAAU,aAAa,IAAI,cAAc,KAAK,WAAW,IAAI;AAAA,UAC7D,mBAAmB;AAAA,QAAA,CACtB;AAAA,MACL,UAAA;AACI,gBAAA;AAAA,MACJ;AAAA,IACJ;AAAA,IACA,UAAU;AACN,aAAO,KAAK,UAAA;AAAA,IAChB;AAAA,EAAA;AAER;"}
1
+ {"version":3,"file":"text-generation.js","sources":["../../src/runners/text-generation.ts"],"sourcesContent":["/**\n * The built-in text runner — the generic `pipeline('text-generation')` path this\n * provider has always run, extracted from the worker so it is one runner among others.\n *\n * It flattens the conversation to `{ role, content: string }` turns (the tokenizer applies\n * the chat template). Two things it cannot carry, it SAYS: tool turns (their wire syntax is\n * model-specific) and image parts (a text model has no eyes). The second used to vanish\n * silently — a photo attached to a text model produced an answer that pretended — and\n * that silence, not the limitation, was the defect.\n */\n\n// Types only. A runner runs INSIDE the worker, and the worker bundle has no way to\n// resolve `@aparte/core` at runtime (an import map does not reach a worker) — so a value\n// import here is not externalised, it is inlined: the first build that imported\n// `contentToText` shipped all of core, components included, in a 426 kB runner chunk.\nimport type { AparteChatMessage, AparteContentPart } from '@aparte/core';\nimport type { CreateRunner, RunnerContext, RunnerGenerateInput } from './types.js';\nimport { TOOL_TURNS_DROPPED, generationOptions, interruptOn, loadOptions, textStreamer } from './shared.js';\n\ntype SimpleMessage = { role: 'user' | 'assistant' | 'system'; content: string };\n\nexport const IMAGES_DROPPED =\n 'This model has no vision runner: image parts were dropped from the prompt, so the model '\n + 'answers as if there were none. Register the model with task: \"image-text-to-text\", or '\n + 'point `runner` at a module of your own.';\n\n/** The text parts of a message, joined — what a text-only chat template can take. */\nfunction textOf(content: string | AparteContentPart[]): string {\n if (typeof content === 'string') return content;\n return content\n .filter((p): p is Extract<AparteContentPart, { type: 'text' }> => p.type === 'text')\n .map((p) => p.text)\n .join('');\n}\n\n/** Flatten to what the chat template takes; say what was left out. */\nexport function flattenForChatTemplate(messages: AparteChatMessage[], warn: RunnerContext['warn']): SimpleMessage[] {\n const result: SimpleMessage[] = [];\n let droppedImages = 0;\n let droppedToolTurns = 0;\n for (const m of messages) {\n if (m.role === 'user' || m.role === 'assistant' || m.role === 'system') {\n // An assistant's calls ride on an `assistant` message, which passes the role test\n // above and, when the model said nothing before them, carries no text either — so\n // without this the whole turn leaves the prompt with nothing said.\n if (m.toolCalls?.length) droppedToolTurns++;\n if (Array.isArray(m.content)) droppedImages += m.content.filter((p) => p.type === 'image').length;\n const text = textOf(m.content);\n if (text) result.push({ role: m.role, content: text });\n } else {\n droppedToolTurns++;\n }\n }\n if (droppedImages > 0) warn(IMAGES_DROPPED);\n if (droppedToolTurns > 0) warn(TOOL_TURNS_DROPPED);\n return result;\n}\n\n/** The pipeline, as this runner calls it. Transformers.js types it per task; this is the one task. */\ninterface TextPipeline {\n (messages: SimpleMessage[], options: Record<string, unknown>): Promise<unknown>;\n tokenizer: unknown;\n dispose?: () => Promise<void>;\n}\n\nexport const createRunner: CreateRunner = async (ctx) => {\n const pipeline = ctx.transformers.pipeline as unknown as (task: string, model: string, options: unknown) => Promise<TextPipeline>;\n const pipe = await pipeline('text-generation', ctx.modelId, loadOptions(ctx));\n\n return {\n async generate({ messages, options, emit, signal }: RunnerGenerateInput): Promise<void> {\n const { stopping, release } = interruptOn(signal, ctx.transformers);\n try {\n await pipe(flattenForChatTemplate(messages, ctx.warn), {\n ...generationOptions(options),\n streamer: textStreamer(ctx.transformers, pipe.tokenizer, emit),\n stopping_criteria: stopping,\n });\n } finally {\n release();\n }\n },\n dispose() {\n return pipe.dispose?.();\n },\n };\n};\n"],"names":[],"mappings":";AAqBO,MAAM,iBACT;AAKJ,SAAS,OAAO,SAA+C;AAC3D,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,SAAO,QACF,OAAO,CAAC,MAAyD,EAAE,SAAS,MAAM,EAClF,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,KAAK,EAAE;AAChB;AAGO,SAAS,uBAAuB,UAA+B,MAA8C;AAChH,QAAM,SAA0B,CAAA;AAChC,MAAI,gBAAgB;AACpB,MAAI,mBAAmB;AACvB,aAAW,KAAK,UAAU;AACtB,QAAI,EAAE,SAAS,UAAU,EAAE,SAAS,eAAe,EAAE,SAAS,UAAU;AAIpE,UAAI,EAAE,WAAW,OAAQ;AACzB,UAAI,MAAM,QAAQ,EAAE,OAAO,EAAG,kBAAiB,EAAE,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE;AAC3F,YAAM,OAAO,OAAO,EAAE,OAAO;AAC7B,UAAI,aAAa,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,MAAM;AAAA,IACzD,OAAO;AACH;AAAA,IACJ;AAAA,EACJ;AACA,MAAI,gBAAgB,EAAG,MAAK,cAAc;AAC1C,MAAI,mBAAmB,EAAG,MAAK,kBAAkB;AACjD,SAAO;AACX;AASO,MAAM,eAA6B,OAAO,QAAQ;AACrD,QAAM,WAAW,IAAI,aAAa;AAClC,QAAM,OAAO,MAAM,SAAS,mBAAmB,IAAI,SAAS,YAAY,GAAG,CAAC;AAE5E,SAAO;AAAA,IACH,MAAM,SAAS,EAAE,UAAU,SAAS,MAAM,UAA8C;AACpF,YAAM,EAAE,UAAU,QAAA,IAAY,YAAY,QAAQ,IAAI,YAAY;AAClE,UAAI;AACA,cAAM,KAAK,uBAAuB,UAAU,IAAI,IAAI,GAAG;AAAA,UACnD,GAAG,kBAAkB,OAAO;AAAA,UAC5B,UAAU,aAAa,IAAI,cAAc,KAAK,WAAW,IAAI;AAAA,UAC7D,mBAAmB;AAAA,QAAA,CACtB;AAAA,MACL,UAAA;AACI,gBAAA;AAAA,MACJ;AAAA,IACJ;AAAA,IACA,UAAU;AACN,aAAO,KAAK,UAAA;AAAA,IAChB;AAAA,EAAA;AAER;"}
@@ -1 +1 @@
1
- {"version":3,"file":"worker-host.d.ts","sourceRoot":"","sources":["../src/worker-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAsB,MAAM,oBAAoB,CAAC;AAEhK,UAAU,eAAe;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,SAAS,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C,CAAC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,eAAe,CAAC,GACnD,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAE,GAAG,eAAe,CAAC,GAC/G;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9B,CAAC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,eAAe,CAAC,CAAC;AAE1F,MAAM,MAAM,UAAU,GAChB,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,cAAc,CAAC,GACnD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,WAAW,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/E,MAAM,WAAW,cAAc;IAC3B,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,0FAA0F;IAC1F,gBAAgB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAClE,sGAAsG;IACtG,YAAY,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CACvF;AAID,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG;IAAE,SAAS,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAAA;CAAE,CAkG1F"}
1
+ {"version":3,"file":"worker-host.d.ts","sourceRoot":"","sources":["../src/worker-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAsB,MAAM,oBAAoB,CAAC;AAEhK,UAAU,eAAe;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,SAAS,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C,CAAC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,eAAe,CAAC,GACnD,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAE,GAAG,eAAe,CAAC,GAC/G;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9B,CAAC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,eAAe,CAAC,CAAC;AAE1F,MAAM,MAAM,UAAU,GAChB,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,cAAc,CAAC,GACnD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,WAAW,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/E,MAAM,WAAW,cAAc;IAC3B,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,0FAA0F;IAC1F,gBAAgB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAClE,sGAAsG;IACtG,YAAY,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CACvF;AAID,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG;IAAE,SAAS,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAAA;CAAE,CA2I1F"}
package/dist/worker.js CHANGED
@@ -43,11 +43,15 @@ function createWorkerHost(deps) {
43
43
  }
44
44
  }
45
45
  async function handleGenerate(msg) {
46
- const controller = new AbortController();
46
+ const controller = generates.get(msg.id) ?? new AbortController();
47
47
  generates.set(msg.id, controller);
48
48
  let usage;
49
49
  let closed = false;
50
50
  try {
51
+ if (controller.signal.aborted) {
52
+ deps.post({ type: "gen-done", id: msg.id });
53
+ return;
54
+ }
51
55
  const runner = await ensureRunner(msg, msg.id);
52
56
  await runner.generate({
53
57
  messages: msg.messages,
@@ -87,6 +91,11 @@ function createWorkerHost(deps) {
87
91
  deps.post({ type: "command-result", id: msg.id, error: errorText(err, "Command failed") });
88
92
  }
89
93
  }
94
+ let chain = Promise.resolve();
95
+ const serialize = (work) => {
96
+ chain = chain.then(work).catch(() => {
97
+ });
98
+ };
90
99
  return {
91
100
  onMessage(msg) {
92
101
  switch (msg.type) {
@@ -94,16 +103,25 @@ function createWorkerHost(deps) {
94
103
  moduleUrl = msg.transformersUrl;
95
104
  break;
96
105
  case "prepare":
97
- void handlePrepare(msg);
106
+ serialize(() => handlePrepare(msg));
98
107
  break;
108
+ /*
109
+ * The abort intent is registered HERE, when the message arrives, not
110
+ * where the generate runs: a generate is chained, so it can wait for
111
+ * as long as a model load takes, and a `cancel` reaching the worker in
112
+ * that window found no controller to abort. The generate then started
113
+ * as if nothing had happened and streamed tokens into a reply the user
114
+ * had already stopped.
115
+ */
99
116
  case "generate":
100
- void handleGenerate(msg);
117
+ generates.set(msg.id, new AbortController());
118
+ serialize(() => handleGenerate(msg));
101
119
  break;
102
120
  case "cancel":
103
121
  generates.get(msg.id)?.abort();
104
122
  break;
105
123
  case "command":
106
- void handleCommand(msg);
124
+ serialize(() => handleCommand(msg));
107
125
  break;
108
126
  }
109
127
  }
@@ -1 +1 @@
1
- {"version":3,"file":"worker.js","sources":["../src/worker-host.ts","../src/worker.ts"],"sourcesContent":["/**\n * The worker's logic, as a function of its three seams.\n *\n * `worker.ts` is the two-line shell that binds this to `self`; everything it decides is\n * here, with `post`, `loadTransformers` and `importRunner` injected — so the protocol is\n * tested with fakes where no Worker and no model can load (`worker-host.test.ts`).\n *\n * The protocol (main ⇄ worker):\n *\n * → init { transformersUrl? } once, first\n * → prepare { id, modelId, task?, runner?, dtype?, device? }\n * → generate { id, modelId, messages, options, task?, runner?, dtype?, device? }\n * → cancel { id }\n * → command { id, modelId, name, payload, task?, runner?, dtype?, device? }\n * ← progress { id, status, file?, progress?, detail? }\n * ← pipeline-ready { modelId } a runner is up\n * ← prepare-error { id, message }\n * ← gen-event { id, event } text | thinking | tool_use\n * ← gen-done { id, usage? } the host closes; `done` is never forwarded raw\n * ← gen-error { id, message } emitted or thrown\n * ← warning { message } once per distinct text\n * ← command-result { id, result } | { id, error }\n *\n * One runner is resident at a time, keyed by model AND runner: a switch disposes the\n * previous one first, which is the \"one pipeline per tab\" rule this package has always\n * kept — a local model is gigabytes, and two of them resident is the failure to avoid.\n */\n\nimport type { AparteChatMessage, AparteStreamEvent, AparteUsage } from '@aparte/core';\nimport type { BuiltInRunner, Device, Dtype, GenerationOptions, RunnerModule, RunnerProgress, TransformersModule, TransformersRunner } from './runners/types.js';\n\ninterface RunnerSelection {\n modelId: string;\n task?: BuiltInRunner;\n runner?: string;\n dtype?: Dtype;\n device?: Device;\n}\n\nexport type InMessage =\n | { type: 'init'; transformersUrl?: string }\n | ({ type: 'prepare'; id: string } & RunnerSelection)\n | ({ type: 'generate'; id: string; messages: AparteChatMessage[]; options: GenerationOptions } & RunnerSelection)\n | { type: 'cancel'; id: string }\n | ({ type: 'command'; id: string; name: string; payload: unknown } & RunnerSelection);\n\nexport type OutMessage =\n | ({ type: 'progress'; id: string } & RunnerProgress)\n | { type: 'pipeline-ready'; modelId: string }\n | { type: 'prepare-error'; id: string; message: string }\n | { type: 'gen-event'; id: string; event: AparteStreamEvent }\n | { type: 'gen-done'; id: string; usage?: AparteUsage }\n | { type: 'gen-error'; id: string; message: string }\n | { type: 'warning'; message: string }\n | { type: 'command-result'; id: string; result?: unknown; error?: string };\n\nexport interface WorkerHostDeps {\n post(message: OutMessage): void;\n /** Resolve Transformers.js — the bundled specifier first, else the URL `init` carried. */\n loadTransformers(moduleUrl?: string): Promise<TransformersModule>;\n /** Import the runner module a selection names: a built-in by `task`, a custom one by `runner` URL. */\n importRunner(spec: { task: BuiltInRunner; runner?: string }): Promise<RunnerModule>;\n}\n\nconst errorText = (err: unknown, fallback: string): string => (err instanceof Error && err.message) || fallback;\n\nexport function createWorkerHost(deps: WorkerHostDeps): { onMessage(msg: InMessage): void } {\n let moduleUrl: string | undefined;\n let current: { key: string; runner: TransformersRunner } | null = null;\n const generates = new Map<string, AbortController>();\n const warned = new Set<string>();\n\n const warn = (message: string): void => {\n if (warned.has(message)) return;\n warned.add(message);\n deps.post({ type: 'warning', message });\n };\n\n async function ensureRunner(sel: RunnerSelection, progressId?: string): Promise<TransformersRunner> {\n const task = sel.task ?? 'text-generation';\n const key = `${sel.modelId}::${sel.runner ?? task}`;\n if (current?.key === key) return current.runner;\n if (current) {\n const previous = current;\n current = null;\n await previous.runner.dispose?.();\n }\n const transformers = await deps.loadTransformers(moduleUrl);\n const { createRunner } = await deps.importRunner({ task, runner: sel.runner });\n const runner = await createRunner({\n transformers,\n modelId: sel.modelId,\n dtype: sel.dtype,\n device: sel.device,\n progress: (p) => { if (progressId) deps.post({ type: 'progress', id: progressId, ...p }); },\n warn,\n });\n current = { key, runner };\n deps.post({ type: 'pipeline-ready', modelId: sel.modelId });\n return runner;\n }\n\n async function handlePrepare(msg: Extract<InMessage, { type: 'prepare' }>): Promise<void> {\n try {\n await ensureRunner(msg, msg.id);\n deps.post({ type: 'progress', id: msg.id, status: 'ready' });\n } catch (err) {\n deps.post({ type: 'prepare-error', id: msg.id, message: errorText(err, 'Failed to load model') });\n }\n }\n\n async function handleGenerate(msg: Extract<InMessage, { type: 'generate' }>): Promise<void> {\n const controller = new AbortController();\n generates.set(msg.id, controller);\n let usage: AparteUsage | undefined;\n let closed = false;\n try {\n const runner = await ensureRunner(msg, msg.id);\n await runner.generate({\n messages: msg.messages,\n options: msg.options,\n signal: controller.signal,\n emit: (event) => {\n if (closed) return;\n // `done` and `error` close the stream, and closing is the host's: it\n // releases the queue slot on the main thread through gen-done/gen-error.\n if (event.type === 'done') { usage = event.usage; return; }\n if (event.type === 'error') { closed = true; deps.post({ type: 'gen-error', id: msg.id, message: event.message }); return; }\n deps.post({ type: 'gen-event', id: msg.id, event });\n },\n });\n if (!closed) deps.post({ type: 'gen-done', id: msg.id, ...(usage ? { usage } : {}) });\n } catch (err) {\n if (!closed) deps.post({ type: 'gen-error', id: msg.id, message: errorText(err, 'Generation failed') });\n } finally {\n generates.delete(msg.id);\n }\n }\n\n async function handleCommand(msg: Extract<InMessage, { type: 'command' }>): Promise<void> {\n try {\n const runner = await ensureRunner(msg);\n if (!runner.command) {\n deps.post({ type: 'command-result', id: msg.id, error: `This runner has no command handler (asked for \"${msg.name}\")` });\n return;\n }\n const result = await runner.command(msg.name, msg.payload);\n deps.post({ type: 'command-result', id: msg.id, result });\n } catch (err) {\n deps.post({ type: 'command-result', id: msg.id, error: errorText(err, 'Command failed') });\n }\n }\n\n return {\n onMessage(msg: InMessage): void {\n switch (msg.type) {\n case 'init': moduleUrl = msg.transformersUrl; break;\n case 'prepare': void handlePrepare(msg); break;\n case 'generate': void handleGenerate(msg); break;\n case 'cancel': generates.get(msg.id)?.abort(); break;\n case 'command': void handleCommand(msg); break;\n }\n },\n };\n}\n","/**\n * The Transformers.js inference worker — the shell.\n *\n * Runs entirely off the main thread. What it decides lives in `worker-host.ts` (the\n * protocol, the one-runner-at-a-time rule, cancel, warnings); what a model IS lives in a\n * runner (`runners/`). This file binds the two to `self` and owns the two things only a\n * real worker can do: resolve Transformers.js, and import a runner module.\n */\n\nimport type { BuiltInRunner, RunnerModule, TransformersModule } from './runners/types.js';\nimport { createWorkerHost } from './worker-host.js';\n\n/**\n * Where Transformers.js comes from, resolved once, from whichever path has it.\n *\n * A static `import … from '@huggingface/transformers'` is unresolvable in a worker\n * served without a bundler: an import map lives on the DOCUMENT and, by spec, does not\n * reach a worker — so the page can map the specifier for itself and the worker still\n * cannot. The two paths, in order:\n *\n * 1. `import('@huggingface/transformers')` — a bare specifier, statically visible, so a\n * consumer's bundler resolves and bundles the peer exactly as it did before.\n * 2. the absolute URL the main thread read from the page's own import map and sent in\n * the first message — the CDN path, where that map is the consumer's manifest.\n *\n * The order matters: a bundled app must never reach for the network copy. Whichever\n * path won is the module every runner receives as `ctx.transformers` — one copy per\n * worker, the version the CONSUMER installed or pinned.\n */\nlet _tf: Promise<TransformersModule> | null = null;\n\nfunction loadTransformers(moduleUrl?: string): Promise<TransformersModule> {\n _tf ??= (async () => {\n let mod: TransformersModule;\n try {\n mod = await import('@huggingface/transformers');\n } catch (bundlerPathFailed) {\n if (!moduleUrl) throw bundlerPathFailed;\n mod = await import(/* @vite-ignore */ moduleUrl) as TransformersModule;\n }\n // Fetch weights from the Hugging Face hub (not local paths) and cache them in the\n // browser Cache API — this is what `listCachedModels()` scans on the main thread.\n mod.env.allowLocalModels = false;\n mod.env.useBrowserCache = true;\n return mod;\n })();\n return _tf;\n}\n\n/**\n * The runners this package ships, each behind a dynamic import so the bundler splits\n * it into its own chunk and a page loads only the one its model asks for. The imports\n * are RELATIVE on purpose: a module script resolves them against its own URL, so they\n * follow the worker wherever it is served from — the same origin, a CDN, or through the\n * blob shim `_spawnWorker` builds for a cross-origin copy.\n */\nconst BUILT_IN: Record<BuiltInRunner, () => Promise<RunnerModule>> = {\n 'text-generation': () => import('./runners/text-generation.js'),\n 'image-text-to-text': () => import('./runners/image-text-to-text.js'),\n};\n\nfunction importRunner({ task, runner }: { task: BuiltInRunner; runner?: string }): Promise<RunnerModule> {\n // A custom runner is an absolute URL by the time it gets here (the main thread\n // resolved it against the page), and it wins over `task`.\n if (runner) return import(/* @vite-ignore */ runner) as Promise<RunnerModule>;\n return BUILT_IN[task]();\n}\n\n// DOM's `Worker` interface types `postMessage` + typed `addEventListener('message')`,\n// which is enough for the worker scope — avoids pulling the WebWorker lib (it clashes\n// with DOM's global `postMessage`).\nconst ctx = self as unknown as Worker;\n\nconst host = createWorkerHost({\n post: (message) => { ctx.postMessage(message); },\n loadTransformers,\n importRunner,\n});\n\nctx.addEventListener('message', (event: MessageEvent) => { host.onMessage(event.data); });\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"worker.js","sources":["../src/worker-host.ts","../src/worker.ts"],"sourcesContent":["/**\n * The worker's logic, as a function of its three seams.\n *\n * `worker.ts` is the two-line shell that binds this to `self`; everything it decides is\n * here, with `post`, `loadTransformers` and `importRunner` injected — so the protocol is\n * tested with fakes where no Worker and no model can load (`worker-host.test.ts`).\n *\n * The protocol (main ⇄ worker):\n *\n * → init { transformersUrl? } once, first\n * → prepare { id, modelId, task?, runner?, dtype?, device? }\n * → generate { id, modelId, messages, options, task?, runner?, dtype?, device? }\n * → cancel { id }\n * → command { id, modelId, name, payload, task?, runner?, dtype?, device? }\n * ← progress { id, status, file?, progress?, detail? }\n * ← pipeline-ready { modelId } a runner is up\n * ← prepare-error { id, message }\n * ← gen-event { id, event } text | thinking | tool_use\n * ← gen-done { id, usage? } the host closes; `done` is never forwarded raw\n * ← gen-error { id, message } emitted or thrown\n * ← warning { message } once per distinct text\n * ← command-result { id, result } | { id, error }\n *\n * One runner is resident at a time, keyed by model AND runner: a switch disposes the\n * previous one first, which is the \"one pipeline per tab\" rule this package has always\n * kept — a local model is gigabytes, and two of them resident is the failure to avoid.\n */\n\nimport type { AparteChatMessage, AparteStreamEvent, AparteUsage } from '@aparte/core';\nimport type { BuiltInRunner, Device, Dtype, GenerationOptions, RunnerModule, RunnerProgress, TransformersModule, TransformersRunner } from './runners/types.js';\n\ninterface RunnerSelection {\n modelId: string;\n task?: BuiltInRunner;\n runner?: string;\n dtype?: Dtype;\n device?: Device;\n}\n\nexport type InMessage =\n | { type: 'init'; transformersUrl?: string }\n | ({ type: 'prepare'; id: string } & RunnerSelection)\n | ({ type: 'generate'; id: string; messages: AparteChatMessage[]; options: GenerationOptions } & RunnerSelection)\n | { type: 'cancel'; id: string }\n | ({ type: 'command'; id: string; name: string; payload: unknown } & RunnerSelection);\n\nexport type OutMessage =\n | ({ type: 'progress'; id: string } & RunnerProgress)\n | { type: 'pipeline-ready'; modelId: string }\n | { type: 'prepare-error'; id: string; message: string }\n | { type: 'gen-event'; id: string; event: AparteStreamEvent }\n | { type: 'gen-done'; id: string; usage?: AparteUsage }\n | { type: 'gen-error'; id: string; message: string }\n | { type: 'warning'; message: string }\n | { type: 'command-result'; id: string; result?: unknown; error?: string };\n\nexport interface WorkerHostDeps {\n post(message: OutMessage): void;\n /** Resolve Transformers.js — the bundled specifier first, else the URL `init` carried. */\n loadTransformers(moduleUrl?: string): Promise<TransformersModule>;\n /** Import the runner module a selection names: a built-in by `task`, a custom one by `runner` URL. */\n importRunner(spec: { task: BuiltInRunner; runner?: string }): Promise<RunnerModule>;\n}\n\nconst errorText = (err: unknown, fallback: string): string => (err instanceof Error && err.message) || fallback;\n\nexport function createWorkerHost(deps: WorkerHostDeps): { onMessage(msg: InMessage): void } {\n let moduleUrl: string | undefined;\n let current: { key: string; runner: TransformersRunner } | null = null;\n const generates = new Map<string, AbortController>();\n const warned = new Set<string>();\n\n const warn = (message: string): void => {\n if (warned.has(message)) return;\n warned.add(message);\n deps.post({ type: 'warning', message });\n };\n\n async function ensureRunner(sel: RunnerSelection, progressId?: string): Promise<TransformersRunner> {\n const task = sel.task ?? 'text-generation';\n const key = `${sel.modelId}::${sel.runner ?? task}`;\n if (current?.key === key) return current.runner;\n if (current) {\n const previous = current;\n current = null;\n await previous.runner.dispose?.();\n }\n const transformers = await deps.loadTransformers(moduleUrl);\n const { createRunner } = await deps.importRunner({ task, runner: sel.runner });\n const runner = await createRunner({\n transformers,\n modelId: sel.modelId,\n dtype: sel.dtype,\n device: sel.device,\n progress: (p) => { if (progressId) deps.post({ type: 'progress', id: progressId, ...p }); },\n warn,\n });\n current = { key, runner };\n deps.post({ type: 'pipeline-ready', modelId: sel.modelId });\n return runner;\n }\n\n async function handlePrepare(msg: Extract<InMessage, { type: 'prepare' }>): Promise<void> {\n try {\n await ensureRunner(msg, msg.id);\n deps.post({ type: 'progress', id: msg.id, status: 'ready' });\n } catch (err) {\n deps.post({ type: 'prepare-error', id: msg.id, message: errorText(err, 'Failed to load model') });\n }\n }\n\n async function handleGenerate(msg: Extract<InMessage, { type: 'generate' }>): Promise<void> {\n // `onMessage` registered the controller when the message arrived; this is the\n // same one, so a `cancel` that landed while the generate waited its turn is\n // already recorded on it.\n const controller = generates.get(msg.id) ?? new AbortController();\n generates.set(msg.id, controller);\n let usage: AparteUsage | undefined;\n let closed = false;\n try {\n if (controller.signal.aborted) {\n // Cancelled before it started: do not load a model in order to\n // interrupt it a moment later. `gen-done` still goes out — it is what\n // releases the main thread's queue slot.\n deps.post({ type: 'gen-done', id: msg.id });\n return;\n }\n const runner = await ensureRunner(msg, msg.id);\n await runner.generate({\n messages: msg.messages,\n options: msg.options,\n signal: controller.signal,\n emit: (event) => {\n if (closed) return;\n // `done` and `error` close the stream, and closing is the host's: it\n // releases the queue slot on the main thread through gen-done/gen-error.\n if (event.type === 'done') { usage = event.usage; return; }\n if (event.type === 'error') { closed = true; deps.post({ type: 'gen-error', id: msg.id, message: event.message }); return; }\n deps.post({ type: 'gen-event', id: msg.id, event });\n },\n });\n if (!closed) deps.post({ type: 'gen-done', id: msg.id, ...(usage ? { usage } : {}) });\n } catch (err) {\n if (!closed) deps.post({ type: 'gen-error', id: msg.id, message: errorText(err, 'Generation failed') });\n } finally {\n generates.delete(msg.id);\n }\n }\n\n async function handleCommand(msg: Extract<InMessage, { type: 'command' }>): Promise<void> {\n try {\n const runner = await ensureRunner(msg);\n if (!runner.command) {\n deps.post({ type: 'command-result', id: msg.id, error: `This runner has no command handler (asked for \"${msg.name}\")` });\n return;\n }\n const result = await runner.command(msg.name, msg.payload);\n deps.post({ type: 'command-result', id: msg.id, result });\n } catch (err) {\n deps.post({ type: 'command-result', id: msg.id, error: errorText(err, 'Command failed') });\n }\n }\n\n /*\n * ONE chain for every message that touches the resident runner.\n *\n * `prepare`, `generate` and `command` all go through `ensureRunner`, which on a\n * key change disposes the previous runner — and they used to run concurrently,\n * so a `prepare` for another model disposed the pipeline an in-flight\n * `generate` was executing (with the real runner, `pipe.dispose()` on the ONNX\n * session mid-token), and two `prepare`s in one tick each saw `current === null`\n * across three awaits and left two multi-GB models resident, the first\n * unreachable for cleanup. The main thread already chains its generates for the\n * same reason; `prepare` was on no chain at all.\n *\n * `cancel` is deliberately NOT chained: its whole job is to reach the generate\n * that is running now.\n *\n * Every handler settles its own errors (each posts a `*-error` message), so the\n * chain cannot be poisoned; the trailing catch is belt and braces.\n */\n let chain: Promise<void> = Promise.resolve();\n const serialize = (work: () => Promise<void>): void => {\n chain = chain.then(work).catch(() => { /* handlers report their own failures */ });\n };\n\n return {\n onMessage(msg: InMessage): void {\n switch (msg.type) {\n case 'init': moduleUrl = msg.transformersUrl; break;\n case 'prepare': serialize(() => handlePrepare(msg)); break;\n /*\n * The abort intent is registered HERE, when the message arrives, not\n * where the generate runs: a generate is chained, so it can wait for\n * as long as a model load takes, and a `cancel` reaching the worker in\n * that window found no controller to abort. The generate then started\n * as if nothing had happened and streamed tokens into a reply the user\n * had already stopped.\n */\n case 'generate': generates.set(msg.id, new AbortController()); serialize(() => handleGenerate(msg)); break;\n case 'cancel': generates.get(msg.id)?.abort(); break;\n case 'command': serialize(() => handleCommand(msg)); break;\n }\n },\n };\n}\n","/**\n * The Transformers.js inference worker — the shell.\n *\n * Runs entirely off the main thread. What it decides lives in `worker-host.ts` (the\n * protocol, the one-runner-at-a-time rule, cancel, warnings); what a model IS lives in a\n * runner (`runners/`). This file binds the two to `self` and owns the two things only a\n * real worker can do: resolve Transformers.js, and import a runner module.\n */\n\nimport type { BuiltInRunner, RunnerModule, TransformersModule } from './runners/types.js';\nimport { createWorkerHost } from './worker-host.js';\n\n/**\n * Where Transformers.js comes from, resolved once, from whichever path has it.\n *\n * A static `import … from '@huggingface/transformers'` is unresolvable in a worker\n * served without a bundler: an import map lives on the DOCUMENT and, by spec, does not\n * reach a worker — so the page can map the specifier for itself and the worker still\n * cannot. The two paths, in order:\n *\n * 1. `import('@huggingface/transformers')` — a bare specifier, statically visible, so a\n * consumer's bundler resolves and bundles the peer exactly as it did before.\n * 2. the absolute URL the main thread read from the page's own import map and sent in\n * the first message — the CDN path, where that map is the consumer's manifest.\n *\n * The order matters: a bundled app must never reach for the network copy. Whichever\n * path won is the module every runner receives as `ctx.transformers` — one copy per\n * worker, the version the CONSUMER installed or pinned.\n */\nlet _tf: Promise<TransformersModule> | null = null;\n\nfunction loadTransformers(moduleUrl?: string): Promise<TransformersModule> {\n _tf ??= (async () => {\n let mod: TransformersModule;\n try {\n mod = await import('@huggingface/transformers');\n } catch (bundlerPathFailed) {\n if (!moduleUrl) throw bundlerPathFailed;\n mod = await import(/* @vite-ignore */ moduleUrl) as TransformersModule;\n }\n // Fetch weights from the Hugging Face hub (not local paths) and cache them in the\n // browser Cache API — this is what `listCachedModels()` scans on the main thread.\n mod.env.allowLocalModels = false;\n mod.env.useBrowserCache = true;\n return mod;\n })();\n return _tf;\n}\n\n/**\n * The runners this package ships, each behind a dynamic import so the bundler splits\n * it into its own chunk and a page loads only the one its model asks for. The imports\n * are RELATIVE on purpose: a module script resolves them against its own URL, so they\n * follow the worker wherever it is served from — the same origin, a CDN, or through the\n * blob shim `_spawnWorker` builds for a cross-origin copy.\n */\nconst BUILT_IN: Record<BuiltInRunner, () => Promise<RunnerModule>> = {\n 'text-generation': () => import('./runners/text-generation.js'),\n 'image-text-to-text': () => import('./runners/image-text-to-text.js'),\n};\n\nfunction importRunner({ task, runner }: { task: BuiltInRunner; runner?: string }): Promise<RunnerModule> {\n // A custom runner is an absolute URL by the time it gets here (the main thread\n // resolved it against the page), and it wins over `task`.\n if (runner) return import(/* @vite-ignore */ runner) as Promise<RunnerModule>;\n return BUILT_IN[task]();\n}\n\n// DOM's `Worker` interface types `postMessage` + typed `addEventListener('message')`,\n// which is enough for the worker scope — avoids pulling the WebWorker lib (it clashes\n// with DOM's global `postMessage`).\nconst ctx = self as unknown as Worker;\n\nconst host = createWorkerHost({\n post: (message) => { ctx.postMessage(message); },\n loadTransformers,\n importRunner,\n});\n\nctx.addEventListener('message', (event: MessageEvent) => { host.onMessage(event.data); });\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.16.11",
3
+ "version": "0.17.0",
4
4
  "homepage": "https://apartejs.dev/providers/ai/transformers/",
5
5
  "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.",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "node": ">=18"
26
26
  },
27
27
  "peerDependencies": {
28
- "@aparte/core": ">=0.16.11 <1.0.0",
28
+ "@aparte/core": ">=0.17.0 <1.0.0",
29
29
  "@huggingface/transformers": "^4.2.0"
30
30
  },
31
31
  "devDependencies": {
@@ -33,7 +33,7 @@
33
33
  "@types/node": "^22.0.0",
34
34
  "typescript": "^5.4.0",
35
35
  "vite": "^6.0.0",
36
- "@aparte/core": "0.16.11"
36
+ "@aparte/core": "0.17.0"
37
37
  },
38
38
  "keywords": [
39
39
  "ai",