@aparte/provider-transformers 0.24.0 → 0.26.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
@@ -106,9 +106,11 @@ export async function createRunner(ctx) {
106
106
  }
107
107
  ```
108
108
 
109
+ `input.tools` are the tools the request allows (a `RunnerTool`: `name`, `description`,
110
+ `inputSchema`) and `input.toolChoice` its `'auto'` / `'none'` / `{ name }` — absent when it has none.
109
111
  `emit` speaks aparté's stream vocabulary (`text`, `thinking`, `tool_use`, `done`, `error`),
110
112
  `signal` fires on Stop, `ctx.progress` (a `RunnerProgress`) / `ctx.warn` reach the page. The contract is
111
- exported: `TransformersRunner`, `RunnerContext`, `RunnerGenerateInput`, `CreateRunner`, `RunnerModule` (what
113
+ exported: `TransformersRunner`, `RunnerContext`, `RunnerGenerateInput`, `RunnerTool`, `CreateRunner`, `RunnerModule` (what
112
114
  the module exports), `BuiltInRunner` (the two `task` names) and `TransformersModule` (the type of
113
115
  `ctx.transformers`).
114
116
  `runnerCommand(modelId, name, payload)` reaches a runner's `command()` from the page, queued
package/dist/index.d.ts CHANGED
@@ -122,7 +122,7 @@ export declare function getComputeDevice(): ComputeDevice;
122
122
  export declare const TransformersProvider: AparteAIProvider & Required<Pick<AparteAIProvider, 'prepareModel' | 'getModelStatus' | 'chat'>>;
123
123
  export default TransformersProvider;
124
124
  export type { AparteAIProvider, AparteAIModel, ModelStatus, ModelLoadProgress } from '@aparte/core';
125
- export type { TransformersRunner, RunnerContext, RunnerGenerateInput, RunnerProgress, RunnerModule, CreateRunner, BuiltInRunner, TransformersModule, } from './runners/types.js';
125
+ export type { TransformersRunner, RunnerContext, RunnerGenerateInput, RunnerTool, RunnerProgress, RunnerModule, CreateRunner, BuiltInRunner, TransformersModule, } from './runners/types.js';
126
126
  /** Returns the modelId currently loaded in the worker's pipeline, or null. */
127
127
  export declare function getLoadedModelId(): string | null;
128
128
  /**
@@ -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
@@ -348,6 +348,10 @@ const TransformersProvider = {
348
348
  // is the runner's knowledge, not this thread's.
349
349
  messages: request.messages,
350
350
  options,
351
+ // The tools this turn allows, as plain definitions (what crosses to a worker
352
+ // is cloned), and the request's choice among them.
353
+ ...request.tools?.length ? { tools: request.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) } : {},
354
+ ...request.toolChoice ? { toolChoice: request.toolChoice } : {},
351
355
  ..._selection(request.modelId)
352
356
  });
353
357
  };
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 /**\n * The most tokens a reply from this model may run to, unless the request sets its own\n * `maxTokens`. Left unset, a built-in runner stops at 8192. With a built-in runner, whatever\n * the ceiling, a reply also ends where the model's context window does, prompt included, and\n * Stop interrupts it. A runner of your own receives `options.maxTokens` only when the request\n * or this setting gives one, so it sets its own default (the README's example uses 8192).\n */\n maxTokens?: number;\n /** ONNX dtype or per-part dtype map (e.g. `'q4'` or `{ decoder_model_merged: 'q4' }`). */\n dtype?: Dtype;\n /** Not read: where models run is one setting for the page, {@link setComputeDevice}. */\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(). */\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/** 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 downloaded in the browser's Cache API. When\n * exceeded after a new model is ready, the oldest models' weights are deleted. 0 = unlimited.\n * It bounds disk, not memory: one model is loaded at a time whatever the budget.\n *\n * The budget counts and evicts only the models given to {@link registerModel} (and the\n * one just loaded). Other Transformers.js weights in the origin's cache, from your own\n * pipelines or another library, are never counted and never deleted.\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 // Only the models this provider was given count: the Cache API is the origin's,\n // and an app's own Transformers.js pipelines (an embedder, a second library) keep\n // their weights there too. Counting them evicted a model we never loaded.\n const cached = (await listCachedModels())\n .filter(e => _registeredModels.has(e.modelId) || e.modelId === keepModelId);\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/**\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 // Only a budget of one deletes a model the other chat needs: 0 is no limit, and two or\n // more keep both chats' models downloaded.\n const deletes = _maxCachedModels === 1\n ? ` With a cache budget of 1, a switch also deletes the other model's downloaded weights `\n + `and downloads them again; setMaxCachedModels(2) keeps both, if the browser's storage has room.`\n : '';\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: each switch unloads one model and loads the other.${deletes} Point both `\n + `chats at one model to avoid it. 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);\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 /**\n * The registered models. Only those: the Cache API is the origin's, and listing what it\n * held offered an app's own embedder, or another library's model, as a chat model to\n * pick. `listCachedModels()` still reports everything downloaded, for a settings panel.\n */\n async fetchModels(): Promise<AparteAIModel[]> {\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 ?? _registeredModels.get(request.modelId)?.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 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 => _modelIdOfUrl(r.url) === 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\n/**\n * The model a cached file belongs to, read from its Hugging Face resolve path — or null.\n * Every cache lookup matches on this, by equality: a substring match took a longer sibling\n * with it (`Qwen2.5-0.5B` also matched `Qwen2.5-0.5B-Instruct`), so deleting one model\n * deleted the other's weights.\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 */\nfunction _modelIdOfUrl(url: string): string | null {\n const m = url.match(/huggingface\\.co\\/([^/]+\\/[^/]+)\\/resolve\\//);\n return m ? decodeURIComponent(m[1]!) : null;\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 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 = _modelIdOfUrl(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 await Promise.all(\n requests\n .filter(r => _modelIdOfUrl(r.url) === modelId)\n .map(r => cache.delete(r)),\n );\n } catch { /* skip */ }\n }));\n } catch { /* Cache API unavailable */ }\n}\n"],"names":[],"mappings":";AA0DA,IAAI,iBAAqE;AAMlE,SAAS,sBAAsB,OAA0D;AAC5F,mBAAiB;AACrB;AAEA,eAAsB,iBAA2C;AAG7D,QAAM,QAAiB,UAAmD,gBAAgB;AAG1F,MAAI,SAAS;AACb,MAAI,SAAS,WAAW;AACpB,QAAI;AACA,YAAM,UAAU,MAAO,UAAyE,IAAI,eAAA;AACpG,eAAS,YAAY;AAAA,IACzB,QAAQ;AACJ,eAAS;AAAA,IACb;AAAA,EACJ;AAEA,MAAI;AACJ,MAAI,CAAC,UAAU,QAAQ,GAAG;AACtB,WAAO;AAAA,EACX,WAAW,QAAQ,GAAG;AAClB,WAAO;AAAA,EACX,OAAO;AACH,WAAO;AAAA,EACX;AAEA,QAAM,qBAAqB,iBACpB,eAAe,IAAI,KAAK,eAAe,QAAQ,KAChD;AAEN,SAAO,EAAE,QAAQ,OAAO,MAAM,mBAAA;AAClC;AAuCA,MAAM,wCAAwB,IAAA;AAG9B,IAAI,eAAgC,CAAA;AAK7B,SAAS,cAAc,QAAuC;AACjE,oBAAkB,IAAI,OAAO,IAAI,MAAM;AACvC,MAAI,CAAC,aAAa,KAAK,CAAA,MAAK,EAAE,OAAO,OAAO,EAAE,GAAG;AAC7C,mBAAe,CAAC,GAAG,cAAc;AAAA,MAC7B,IAAI,OAAO;AAAA,MACX,MAAM,OAAO;AAAA,MACb,aAAa,OAAO;AAAA,MACpB,cAAc,OAAO;AAAA,IAAA,CACxB;AAAA,EACL;AACJ;AAGA,IAAI,mBAAmB;AAWhB,SAAS,mBAAmB,KAAmB;AAClD,qBAAmB;AACvB;AAGO,SAAS,qBAA6B;AACzC,SAAO;AACX;AASA,IAAI,iBAAgC;AAE7B,SAAS,iBAAiB,GAAwB;AACrD,mBAAiB;AACrB;AAEO,SAAS,mBAAkC;AAC9C,SAAO;AACX;AASA,eAAe,wBAAwB,aAAoC;AACvE,MAAI,qBAAqB,EAAG;AAC5B,MAAI;AAIA,UAAM,UAAU,MAAM,iBAAA,GACjB,OAAO,CAAA,MAAK,kBAAkB,IAAI,EAAE,OAAO,KAAK,EAAE,YAAY,WAAW;AAC9E,UAAM,QAAQ,IAAI,IAAI,gBAAgB,QAAQ;AAC9C,UAAM,SAAS,OAAO,OAAO,CAAA,MAAK,EAAE,YAAY,eAAe,CAAC,MAAM,IAAI,EAAE,OAAO,CAAC;AACpF,UAAM,SAAS,OAAO,SAAS;AAC/B,QAAI,UAAU,EAAG;AAEjB,aAAS,IAAI,GAAG,IAAI,UAAU,IAAI,OAAO,QAAQ,KAAK;AAClD,YAAM,kBAAkB,OAAO,CAAC,EAAG,OAAO;AAAA,IAC9C;AAAA,EACJ,QAAQ;AAAA,EAA0B;AACtC;AAUA,SAAS,WAAW,SAAiG;AACjH,QAAM,SAAS,kBAAkB,IAAI,OAAO;AAC5C,QAAM,SAAS,QAAQ;AACvB,SAAO;AAAA,IACH,MAAM,QAAQ,QAAQ;AAAA,IACtB,GAAI,SAAS,EAAE,QAAQ,OAAO,aAAa,cAAc,SAAS,IAAI,IAAI,QAAQ,SAAS,IAAI,EAAE,KAAA,IAAS,CAAA;AAAA,IAC1G,OAAO,QAAQ;AAAA,IACf,QAAQ;AAAA,EAAA;AAEhB;AAMA,IAAI,UAAyB;AAQ7B,MAAM,uCAAuB,IAAA;AAC7B,MAAM,wCAAwB,IAAA;AAC9B,MAAM,uCAAuB,IAAA;AAM7B,IAAI,iBAAgC,QAAQ,QAAA;AAC5C,MAAM,6CAA6B,IAAA;AAOnC,MAAM,sCAAsB,IAAA;AAC5B,IAAI,yBAAyB;AAG7B,SAAS,mBAAmB,WAAuC;AAC/D,aAAW,MAAM,gBAAgB,OAAA,EAAU,KAAI,OAAO,UAAW,QAAO;AACxE,SAAO;AACX;AAOA,SAAS,iBAAiB,WAAyB;AAC/C,MAAI,uBAAwB;AAC5B,QAAM,QAAQ,mBAAmB,SAAS;AAC1C,MAAI,CAAC,MAAO;AACZ,2BAAyB;AAGzB,QAAM,UAAU,qBAAqB,IAC/B,yLAEA;AACN,UAAQ;AAAA,IACJ,mEAAmE,SAAS,aACtE,KAAK,uIACwD,OAAO;AAAA,EAAA;AAGlF;AAGA,SAAS,qBAAqB,IAAkB;AAC5C,kBAAgB,OAAO,EAAE;AACzB,QAAM,UAAU,uBAAuB,IAAI,EAAE;AAC7C,MAAI,SAAS;AACT,2BAAuB,OAAO,EAAE;AAChC,YAAA;AAAA,EACJ;AACJ;AAGA,IAAI,iBAAgC;AAEpC,IAAI,oBAAmC;AAcvC,MAAM,cAAc;AAGpB,IAAI,iBAAgC;AAmBpC,SAAS,eAAuB;AAO5B,QAAM,MAAM,IAAI,IAAI,aAAa,YAAY,GAAG;AAChD,QAAM,aAAa,OAAO,aAAa,eAAe,IAAI,WAAW,SAAS;AAM9E,QAAM,cAAc,OAAO,SAAS,cAAc,OAAO,IAAI,oBAAoB;AAWjF,MAAI,cAAc,CAAC,YAAa,QAAO,IAAI,OAAO,IAAI,IAAI,eAAe,YAAY,GAAG,GAAG,EAAE,MAAM,UAAU;AAE7G,mBAAiB,IAAI;AAAA,IACjB,IAAI,KAAK,CAAC,UAAU,KAAK,UAAU,IAAI,IAAI,CAAC,GAAG,GAAG,EAAE,MAAM,mBAAmB;AAAA,EAAA;AAEjF,MAAI;AACA,WAAO,IAAI,OAAO,gBAAgB,EAAE,MAAM,UAAU;AAAA,EACxD,SAAS,OAAO;AAKZ,QAAI,gBAAgB,cAAc;AAClC,qBAAiB;AACjB,UAAM,IAAI;AAAA,MACN,gDAAgD,IAAI,MAAM,oQAGzB,OAAO,KAAK,CAAC;AAAA,IAAA;AAAA,EAEtD;AACJ;AAEA,SAAS,qBAA2B;AAChC,MAAI,gBAAgB;AAChB,QAAI,gBAAgB,cAAc;AAClC,qBAAiB;AAAA,EACrB;AACJ;AAgBA,SAAS,iBAAqC;AAC1C,QAAM,UAAW,YAAuE;AACxF,MAAI,OAAO,YAAY,YAAY;AAC/B,QAAI;AACA,YAAM,OAAO,QAAQ,2BAA2B;AAChD,UAAI,QAAQ,YAAY,KAAK,IAAI,EAAG,QAAO;AAAA,IAC/C,QAAQ;AAAA,IAAsC;AAAA,EAClD;AAEA,MAAI;AACA,UAAM,KAAK,SAAS,cAAc,0BAA0B;AAC5D,UAAM,MAAM,IAAI,cAAc,KAAK,MAAM,GAAG,WAAW,IAA4C;AACnG,UAAM,OAAO,KAAK,UAAU,2BAA2B;AACvD,QAAI,KAAM,QAAO,IAAI,IAAI,MAAM,SAAS,IAAI,EAAE;AAAA,EAClD,QAAQ;AAAA,EAA+C;AACvD,SAAO;AACX;AAEA,SAAS,aAAqB;AAC1B,MAAI,CAAC,SAAS;AACV,cAAU,aAAA;AACV,YAAQ,iBAAiB,WAAW,oBAAoB;AACxD,YAAQ,iBAAiB,SAAS,kBAAkB;AACpD,YAAQ,iBAAiB,gBAAgB,kBAAkB;AAG3D,YAAQ,YAAY,EAAE,MAAM,QAAQ,iBAAiB,eAAA,GAAkB;AAAA,EAC3E;AACA,SAAO;AACX;AAOA,SAAS,mBAAmB,GAAgB;AACxC,QAAM,UAAW,GAAkB,WAAW;AAE9C,aAAW,KAAK,iBAAiB,UAAU;AACvC,QAAI;AAAE,QAAE,OAAO,IAAI,MAAM,OAAO,CAAC;AAAA,IAAG,QAAQ;AAAA,IAAe;AAAA,EAC/D;AACA,mBAAiB,MAAA;AAEjB,aAAW,QAAQ,kBAAkB,UAAU;AAC3C,QAAI;AAAE,WAAK,QAAQ,EAAE,MAAM,SAAkB,SAAS;AAAG,WAAK,MAAA;AAAA,IAAS,QACjE;AAAA,IAAe;AAAA,EACzB;AACA,oBAAkB,MAAA;AAClB,aAAW,KAAK,iBAAiB,OAAA,KAAY,OAAO,IAAI,MAAM,OAAO,CAAC;AACtE,mBAAiB,MAAA;AAGjB,aAAW,WAAW,uBAAuB,UAAU;AACnD,QAAI;AAAE,cAAA;AAAA,IAAW,QAAQ;AAAA,IAAe;AAAA,EAC5C;AACA,yBAAuB,MAAA;AACvB,mBAAiB,QAAQ,QAAA;AACzB,kBAAgB,MAAA;AAEhB,mBAAiB;AACjB,sBAAoB;AACpB,MAAI;AAAE,aAAS,UAAA;AAAA,EAAa,QAAQ;AAAA,EAAe;AACnD,YAAU;AACV,qBAAA;AACJ;AAEA,SAAS,qBAAqB,OAA2B;AACrD,QAAM,MAAM,MAAM;AAElB,UAAQ,IAAI,MAAA;AAAA,IACR,KAAK,YAAY;AACb,YAAM,UAAU,iBAAiB,IAAI,IAAI,EAAE;AAC3C,UAAI,CAAC,QAAS;AACd,UAAI,IAAI,WAAW,SAAS;AACxB,gBAAQ,WAAW,EAAE,QAAQ,QAAA,CAAS;AACtC,gBAAQ,QAAA;AACR,yBAAiB,OAAO,IAAI,EAAE;AAAA,MAClC,WAAW,IAAI,WAAW,WAAW;AACjC,gBAAQ,WAAW,EAAE,QAAQ,UAAA,CAAW;AAAA,MAC5C,WAAW,IAAI,WAAW,UAAU;AAChC,gBAAQ,WAAW,EAAE,QAAQ,UAAU,MAAM,IAAI,MAAM,UAAU,IAAI,SAAA,CAAU;AAAA,MACnF,OAAO;AACH,gBAAQ,WAAW,EAAE,QAAQ,eAAe,MAAM,IAAI,MAAM,UAAU,IAAI,SAAA,CAAU;AAAA,MACxF;AACA;AAAA,IACJ;AAAA,IACA,KAAK,iBAAiB;AAClB,YAAM,UAAU,iBAAiB,IAAI,IAAI,EAAE;AAC3C,UAAI,CAAC,QAAS;AACd,cAAQ,OAAO,IAAI,MAAM,IAAI,OAAO,CAAC;AACrC,uBAAiB,OAAO,IAAI,EAAE;AAC9B,UAAI,sBAAsB,QAAQ,QAAS,qBAAoB;AAC/D;AAAA,IACJ;AAAA,IACA,KAAK,kBAAkB;AACnB,uBAAiB,IAAI;AACrB,0BAAoB;AAEpB,WAAK,wBAAwB,IAAI,OAAO;AACxC;AAAA,IACJ;AAAA,IACA,KAAK,aAAa;AAEd,YAAM,OAAO,kBAAkB,IAAI,IAAI,EAAE;AACzC,UAAI,CAAC,KAAM;AACX,WAAK,QAAQ,IAAI,KAAK;AACtB;AAAA,IACJ;AAAA,IACA,KAAK,WAAW;AAEZ,cAAQ,KAAK,kBAAkB,IAAI,OAAO,EAAE;AAC5C;AAAA,IACJ;AAAA,IACA,KAAK,kBAAkB;AACnB,2BAAqB,IAAI,EAAE;AAC3B,YAAM,UAAU,iBAAiB,IAAI,IAAI,EAAE;AAC3C,UAAI,CAAC,QAAS;AACd,uBAAiB,OAAO,IAAI,EAAE;AAC9B,UAAI,IAAI,UAAU,OAAW,SAAQ,OAAO,IAAI,MAAM,IAAI,KAAK,CAAC;AAAA,UAC3D,SAAQ,QAAQ,IAAI,MAAM;AAC/B;AAAA,IACJ;AAAA,IACA,KAAK,YAAY;AACb,2BAAqB,IAAI,EAAE;AAC3B,YAAM,OAAO,kBAAkB,IAAI,IAAI,EAAE;AACzC,UAAI,CAAC,KAAM;AACX,WAAK,QAAQ,EAAE,MAAM,QAAiB,GAAI,IAAI,QAAQ,EAAE,OAAO,IAAI,MAAA,IAAU,CAAA,GAAK;AAClF,WAAK,MAAA;AACL,wBAAkB,OAAO,IAAI,EAAE;AAC/B;AAAA,IACJ;AAAA,IACA,KAAK,aAAa;AACd,2BAAqB,IAAI,EAAE;AAC3B,YAAM,OAAO,kBAAkB,IAAI,IAAI,EAAE;AACzC,UAAI,CAAC,KAAM;AACX,WAAK,QAAQ,EAAE,MAAM,SAAkB,SAAS,IAAI,SAAS;AAC7D,WAAK,MAAA;AACL,wBAAkB,OAAO,IAAI,EAAE;AAC/B;AAAA,IACJ;AAAA,EAAA;AAER;AAgBO,MAAM,uBACwE;AAAA,EACjF,IAAI;AAAA,EAEJ,cAAc;AACV,WAAO;AAAA,MACH,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,OAAO;AAAA,MACP,aAAa;AAAA,MACb,eAAe;AAAA,MACf,SAAS;AAAA,MACT,SAAS;AAAA,IAAA;AAAA,EAEjB;AAAA,EAEA,YAA6B;AACzB,WAAO;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,cAAwC;AAC1C,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,KACF,SACA,SACA,KAC2B;AAC3B,UAAM,YAAY,KAAA;AAClB,UAAM,UAAU;AAAA,MACZ,WAAW,QAAQ,aAAa,kBAAkB,IAAI,QAAQ,OAAO,GAAG;AAAA,MACxE,aAAa,QAAQ;AAAA,MACrB,MAAM,QAAQ;AAAA,IAAA;AAElB,UAAM,SAAS,KAAK;AAKpB,qBAAiB,QAAQ,OAAO;AAChC,oBAAgB,IAAI,WAAW,QAAQ,OAAO;AAC9C,UAAM,eAAe;AACrB,qBAAiB,IAAI,QAAc,CAAC,gBAAgB;AAChD,6BAAuB,IAAI,WAAW,WAAW;AAAA,IACrD,CAAC;AAQD,QAAI,SAAS;AACb,QAAI,UAAU;AACd,UAAM,OAAO,MAAY;AACrB,UAAI,QAAS;AACb,gBAAU;AACV,cAAQ,oBAAoB,SAAS,IAAI;AACzC,YAAM,OAAO,kBAAkB,IAAI,SAAS;AAC5C,wBAAkB,OAAO,SAAS;AAClC,UAAI,QAAQ;AACR,mBAAA,EAAa,YAAY,EAAE,MAAM,UAAU,IAAI,WAAW;AAO1D,YAAI;AAAE,gBAAM,QAAQ,EAAE,MAAM,OAAA,CAAiB;AAAG,gBAAM,MAAA;AAAA,QAAS,QAAQ;AAAA,QAAuB;AAC9F;AAAA,MACJ;AACA,UAAI,CAAC,KAAM;AACX,UAAI;AAAE,aAAK,QAAQ,EAAE,MAAM,SAAkB,SAAS,0CAA0C;AAAG,aAAK,MAAA;AAAA,MAAS,QAC3G;AAAA,MAAuB;AAAA,IACjC;AACA,UAAM,eAAe,MAAY;AAC7B,UAAI,SAAS;AAAE,6BAAqB,SAAS;AAAG;AAAA,MAAQ;AACxD,eAAS;AACT,iBAAA,EAAa,YAAY;AAAA,QACrB,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,SAAS,QAAQ;AAAA;AAAA;AAAA,QAGjB,UAAU,QAAQ;AAAA,QAClB;AAAA,QACA,GAAG,WAAW,QAAQ,OAAO;AAAA,MAAA,CAChC;AAAA,IACL;AAEA,QAAI;AACJ,QAAI,QAAQ,WAAW,OAAO;AAC1B,iBAAW,IAAI,QAAgB,CAAC,SAAS,WAAW;AAChD,YAAI,SAAS;AACb,cAAM,WAAW;AAAA,UACb,SAAS,CAAC,UAA8D;AACpE,gBAAI,MAAM,SAAS,OAAQ,WAAU,MAAM,SAAS;AAAA,qBAC3C,MAAM,SAAS,OAAQ,SAAQ,MAAM;AAAA,qBACrC,MAAM,SAAS,QAAS,QAAO,IAAI,MAAM,MAAM,OAAO,CAAC;AAAA,UACpE;AAAA,UACA,OAAO,MAAM;AAAA,UAAc;AAAA,QAAA;AAE/B,0BAAkB,IAAI,WAAW,QAAQ;AACzC,aAAK,aAAa,KAAK,YAAY;AAAA,MACvC,CAAC;AAAA,IACL,OAAO;AACH,iBAAW,IAAI,eAAe;AAAA,QAC1B,MAAM,MAAM,YAAY;AACpB,4BAAkB,IAAI,WAAW,UAAU;AAC3C,gBAAM;AACN,uBAAA;AAAA,QACJ;AAAA,QACA,SAAS;AAKL,4BAAkB,OAAO,SAAS;AAClC,eAAA;AAAA,QACJ;AAAA,MAAA,CACH;AAAA,IACL;AAGA,QAAI,QAAQ,QAAS,MAAA;AAAA,iBACR,iBAAiB,SAAS,MAAM,EAAE,MAAM,MAAM;AAC3D,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,eAAe,SAAuC;AACxD,QAAI,mBAAmB,QAAS,QAAO;AACvC,QAAI,sBAAsB,QAAS,QAAO;AAC1C,QAAI,YAAY,YAAY;AACxB,UAAI;AACA,cAAM,QAAQ,MAAM,OAAO,KAAA;AAC3B,mBAAW,QAAQ,OAAO;AACtB,gBAAM,QAAQ,MAAM,OAAO,KAAK,IAAI;AACpC,gBAAM,OAAO,MAAM,MAAM,KAAA;AACzB,cAAI,KAAK,KAAK,CAAA,MAAK,cAAc,EAAE,GAAG,MAAM,OAAO,GAAG;AAClD,mBAAO;AAAA,UACX;AAAA,QACJ;AAAA,MACJ,QAAQ;AAAA,MAER;AAAA,IACJ;AACA,WAAO;AAAA,EACX;AAAA,EAEA,MAAM,aAAa,SAAiB,YAA2D;AAC3F,QAAI,mBAAmB,SAAS;AAC5B,iBAAW,EAAE,QAAQ,SAAS;AAC9B;AAAA,IACJ;AAEA,UAAM,YAAY,KAAA;AAClB,wBAAoB;AAEpB,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC1C,uBAAiB,IAAI,WAAW,EAAE,SAAS,YAAY,SAAS,QAAQ;AACxE,mBAAa,YAAY,EAAE,MAAM,WAAW,IAAI,WAAW,SAAS,GAAG,WAAW,OAAO,EAAA,CAAG;AAAA,IAChG,CAAC;AAAA,EACL;AAAA,EAEA,MAAM,YAAY,SAAgC;AAC9C,UAAM,kBAAkB,OAAO;AAAA,EACnC;AACJ;AAoBO,SAAS,mBAAkC;AAC9C,SAAO;AACX;AAQO,SAAS,cAAc,SAAiB,MAAc,SAAoC;AAC7F,QAAM,YAAY,KAAA;AAClB,kBAAgB,IAAI,WAAW,OAAO;AACtC,QAAM,WAAW;AACjB,mBAAiB,IAAI,QAAc,CAAC,gBAAgB;AAChD,2BAAuB,IAAI,WAAW,WAAW;AAAA,EACrD,CAAC;AACD,SAAO,IAAI,QAAiB,CAAC,SAAS,WAAW;AAC7C,qBAAiB,IAAI,WAAW,EAAE,SAAS,QAAQ;AACnD,SAAK,SAAS,KAAK,MAAM;AACrB,iBAAA,EAAa,YAAY,EAAE,MAAM,WAAW,IAAI,WAAW,SAAS,MAAM,SAAS,GAAG,WAAW,OAAO,GAAG;AAAA,IAC/G,CAAC;AAAA,EACL,CAAC;AACL;AAGO,SAAS,kBAAwB;AACpC,WAAS,UAAA;AACT,YAAU;AACV,qBAAA;AACA,mBAAiB;AACjB,sBAAoB;AACpB,aAAW,CAAA,EAAG,CAAC,KAAK,kBAAkB;AAClC,MAAE,OAAO,IAAI,MAAM,mBAAmB,CAAC;AAAA,EAC3C;AACA,mBAAiB,MAAA;AACjB,aAAW,CAAA,EAAG,IAAI,KAAK,mBAAmB;AACtC,QAAI;AAAE,WAAK,QAAQ,EAAE,MAAM,SAAkB,SAAS,qBAAqB;AAAG,WAAK,MAAA;AAAA,IAAS,QAAQ;AAAA,IAAuB;AAAA,EAC/H;AACA,oBAAkB,MAAA;AAClB,aAAW,KAAK,iBAAiB,OAAA,KAAY,OAAO,IAAI,MAAM,mBAAmB,CAAC;AAClF,mBAAiB,MAAA;AAQjB,aAAW,WAAW,uBAAuB,UAAU;AACnD,QAAI;AAAE,cAAA;AAAA,IAAW,QAAQ;AAAA,IAAe;AAAA,EAC5C;AACA,yBAAuB,MAAA;AACvB,mBAAiB,QAAQ,QAAA;AACzB,kBAAgB,MAAA;AAEhB,2BAAyB;AAC7B;AAWA,SAAS,cAAc,KAA4B;AAC/C,QAAM,IAAI,IAAI,MAAM,4CAA4C;AAChE,SAAO,IAAI,mBAAmB,EAAE,CAAC,CAAE,IAAI;AAC3C;AAeA,eAAsB,mBAAgD;AAClE,MAAI,EAAE,YAAY,YAAa,QAAO,CAAA;AAEtC,QAAM,4BAAY,IAAA;AAElB,WAAS,UAAU,SAAyB;AACxC,UAAM,SAAS,kBAAkB,IAAI,OAAO;AAC5C,QAAI,eAAe,OAAO;AAC1B,YAAQ,QAAQ,MAAM,GAAG,EAAE,SAAS,SAAS,QAAQ,MAAM,GAAG;AAAA,EAClE;AAEA,MAAI;AACA,UAAM,aAAa,MAAM,OAAO,KAAA;AAChC,UAAM,QAAQ,IAAI,WAAW,IAAI,OAAO,cAAc;AAClD,UAAI;AACA,cAAM,QAAQ,MAAM,OAAO,KAAK,SAAS;AACzC,cAAM,WAAW,MAAM,MAAM,KAAA;AAC7B,mBAAW,OAAO,UAAU;AACxB,gBAAM,UAAU,cAAc,IAAI,GAAG;AACrC,cAAI,CAAC,QAAS;AACd,cAAI,CAAC,MAAM,IAAI,OAAO,GAAG;AACrB,kBAAM,IAAI,SAAS,EAAE,MAAM,UAAU,OAAO,GAAG,WAAW,GAAG;AAAA,UACjE;AACA,gBAAM,WAAW,MAAM,MAAM,MAAM,GAAG;AACtC,cAAI,CAAC,SAAU;AACf,gBAAM,gBAAgB,SAAS,QAAQ,IAAI,gBAAgB;AAC3D,cAAI,eAAe;AACf,kBAAM,IAAI,OAAO,EAAG,aAAa,SAAS,eAAe,EAAE;AAAA,UAC/D,OAAO;AACH,gBAAI;AACA,oBAAM,OAAO,MAAM,SAAS,MAAA,EAAQ,KAAA;AACpC,oBAAM,IAAI,OAAO,EAAG,aAAa,KAAK;AAAA,YAC1C,QAAQ;AAAA,YAAa;AAAA,UACzB;AAAA,QACJ;AAAA,MACJ,QAAQ;AAAA,MAAgC;AAAA,IAC5C,CAAC,CAAC;AAAA,EACN,QAAQ;AACJ,WAAO,CAAA;AAAA,EACX;AAEA,SAAO,MAAM,KAAK,MAAM,QAAA,CAAS,EAAE,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,UAAA,CAAW,OAAO;AAAA,IACxE;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,mBAAmB;AAAA,EAAA,EAC7B;AACN;AAMA,eAAsB,kBAAkB,SAAgC;AACpE,MAAI,mBAAmB,WAAW,sBAAsB,SAAS;AAC7D,oBAAA;AAAA,EACJ;AACA,MAAI,EAAE,YAAY,YAAa;AAC/B,MAAI;AACA,UAAM,aAAa,MAAM,OAAO,KAAA;AAChC,UAAM,QAAQ,IAAI,WAAW,IAAI,OAAO,cAAc;AAClD,UAAI;AACA,cAAM,QAAQ,MAAM,OAAO,KAAK,SAAS;AACzC,cAAM,WAAW,MAAM,MAAM,KAAA;AAC7B,cAAM,QAAQ;AAAA,UACV,SACK,OAAO,CAAA,MAAK,cAAc,EAAE,GAAG,MAAM,OAAO,EAC5C,IAAI,CAAA,MAAK,MAAM,OAAO,CAAC,CAAC;AAAA,QAAA;AAAA,MAErC,QAAQ;AAAA,MAAa;AAAA,IACzB,CAAC,CAAC;AAAA,EACN,QAAQ;AAAA,EAA8B;AAC1C;"}
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 /**\n * The most tokens a reply from this model may run to, unless the request sets its own\n * `maxTokens`. Left unset, a built-in runner stops at 8192. With a built-in runner, whatever\n * the ceiling, a reply also ends where the model's context window does, prompt included, and\n * Stop interrupts it. A runner of your own receives `options.maxTokens` only when the request\n * or this setting gives one, so it sets its own default (the README's example uses 8192).\n */\n maxTokens?: number;\n /** ONNX dtype or per-part dtype map (e.g. `'q4'` or `{ decoder_model_merged: 'q4' }`). */\n dtype?: Dtype;\n /** Not read: where models run is one setting for the page, {@link setComputeDevice}. */\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(). */\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/** 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 downloaded in the browser's Cache API. When\n * exceeded after a new model is ready, the oldest models' weights are deleted. 0 = unlimited.\n * It bounds disk, not memory: one model is loaded at a time whatever the budget.\n *\n * The budget counts and evicts only the models given to {@link registerModel} (and the\n * one just loaded). Other Transformers.js weights in the origin's cache, from your own\n * pipelines or another library, are never counted and never deleted.\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 // Only the models this provider was given count: the Cache API is the origin's,\n // and an app's own Transformers.js pipelines (an embedder, a second library) keep\n // their weights there too. Counting them evicted a model we never loaded.\n const cached = (await listCachedModels())\n .filter(e => _registeredModels.has(e.modelId) || e.modelId === keepModelId);\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/**\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 // Only a budget of one deletes a model the other chat needs: 0 is no limit, and two or\n // more keep both chats' models downloaded.\n const deletes = _maxCachedModels === 1\n ? ` With a cache budget of 1, a switch also deletes the other model's downloaded weights `\n + `and downloads them again; setMaxCachedModels(2) keeps both, if the browser's storage has room.`\n : '';\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: each switch unloads one model and loads the other.${deletes} Point both `\n + `chats at one model to avoid it. 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);\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 /**\n * The registered models. Only those: the Cache API is the origin's, and listing what it\n * held offered an app's own embedder, or another library's model, as a chat model to\n * pick. `listCachedModels()` still reports everything downloaded, for a settings panel.\n */\n async fetchModels(): Promise<AparteAIModel[]> {\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 ?? _registeredModels.get(request.modelId)?.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 // The tools this turn allows, as plain definitions (what crosses to a worker\n // is cloned), and the request's choice among them.\n ...(request.tools?.length ? { tools: request.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) } : {}),\n ...(request.toolChoice ? { toolChoice: request.toolChoice } : {}),\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 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 => _modelIdOfUrl(r.url) === 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 RunnerTool,\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\n/**\n * The model a cached file belongs to, read from its Hugging Face resolve path — or null.\n * Every cache lookup matches on this, by equality: a substring match took a longer sibling\n * with it (`Qwen2.5-0.5B` also matched `Qwen2.5-0.5B-Instruct`), so deleting one model\n * deleted the other's weights.\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 */\nfunction _modelIdOfUrl(url: string): string | null {\n const m = url.match(/huggingface\\.co\\/([^/]+\\/[^/]+)\\/resolve\\//);\n return m ? decodeURIComponent(m[1]!) : null;\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 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 = _modelIdOfUrl(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 await Promise.all(\n requests\n .filter(r => _modelIdOfUrl(r.url) === modelId)\n .map(r => cache.delete(r)),\n );\n } catch { /* skip */ }\n }));\n } catch { /* Cache API unavailable */ }\n}\n"],"names":[],"mappings":"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"}
@@ -13,11 +13,14 @@
13
13
  * with a free-form `detail`, and can dispose. A research-grade provider that today
14
14
  * carries its own worker fits here without giving up anything of its own.
15
15
  *
16
- * `tool_use` is in what a runner can emit, not in what it is given: `RunnerGenerateInput`
17
- * carries no tool definitions, because the provider sends a model none. A runner that calls
18
- * tools has to know them itself.
16
+ * A runner is given the tools the request allows (`tools`, when the model declares
17
+ * `function_calling` and the app registered some) and the request's `toolChoice`, and can emit
18
+ * `tool_use` for them. The built-in runners take neither: they warn when a turn needs a tool
19
+ * call they cannot carry.
19
20
  */
20
- import type { AparteChatMessage, AparteStreamEvent } from '@aparte/core';
21
+ import type { AparteChatMessage, AparteChatRequest, AparteStreamEvent, AparteTool } from '@aparte/core';
22
+ /** A tool as a runner sees it: what a model needs to call it, no more. */
23
+ export type RunnerTool = Pick<AparteTool, 'name' | 'description' | 'inputSchema'>;
21
24
  /** The Transformers.js module, as the worker resolved it — a runner never imports its own copy. */
22
25
  export type TransformersModule = typeof import('@huggingface/transformers');
23
26
  /** ONNX dtype, or a per-part map (`{ decoder_model_merged: 'q4' }`). */
@@ -54,6 +57,10 @@ export interface RunnerGenerateInput {
54
57
  /** The conversation, WITH its content parts — the runner decides what to keep. */
55
58
  messages: AparteChatMessage[];
56
59
  options: GenerationOptions;
60
+ /** The tools this turn may call — the app's registration, not a list the runner keeps. Absent when there are none. */
61
+ tools?: RunnerTool[];
62
+ /** `'auto'`, `'none'` (answer directly this turn) or a tool the model must call. Absent when the request sets none. */
63
+ toolChoice?: AparteChatRequest['toolChoice'];
57
64
  /** The stream, in aparté's vocabulary. `done` and `error` are the host's to close on. */
58
65
  emit(event: AparteStreamEvent): void;
59
66
  /** Fired when the page cancels; wire it to the model's stop. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runners/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEzE,mGAAmG;AACnG,MAAM,MAAM,kBAAkB,GAAG,cAAc,2BAA2B,CAAC,CAAC;AAE5E,wEAAwE;AACxE,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACpD,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhD,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAErE,MAAM,WAAW,cAAc;IAC3B,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qCAAqC;AACrC,MAAM,WAAW,aAAa;IAC1B,YAAY,EAAE,kBAAkB,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,4GAA4G;IAC5G,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,QAAQ,CAAC,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAClC,kGAAkG;IAClG,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAChC,kFAAkF;IAClF,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,yFAAyF;IACzF,IAAI,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACrC,gEAAgE;IAChE,MAAM,EAAE,WAAW,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IAC/B,wGAAwG;IACxG,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,uFAAuF;IACvF,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,OAAO,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAE/E,oCAAoC;AACpC,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,YAAY,CAAC;CAC9B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runners/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAExG,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,GAAG,aAAa,CAAC,CAAC;AAElF,mGAAmG;AACnG,MAAM,MAAM,kBAAkB,GAAG,cAAc,2BAA2B,CAAC,CAAC;AAE5E,wEAAwE;AACxE,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACpD,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhD,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAErE,MAAM,WAAW,cAAc;IAC3B,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qCAAqC;AACrC,MAAM,WAAW,aAAa;IAC1B,YAAY,EAAE,kBAAkB,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,4GAA4G;IAC5G,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,QAAQ,CAAC,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAClC,kGAAkG;IAClG,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAChC,kFAAkF;IAClF,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,sHAAsH;IACtH,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,uHAAuH;IACvH,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC7C,yFAAyF;IACzF,IAAI,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACrC,gEAAgE;IAChE,MAAM,EAAE,WAAW,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IAC/B,wGAAwG;IACxG,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,uFAAuF;IACvF,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,OAAO,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAE/E,oCAAoC;AACpC,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,YAAY,CAAC;CAC9B"}
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * → init { transformersUrl? } once, first
11
11
  * → prepare { id, modelId, task?, runner?, dtype?, device? }
12
- * → generate { id, modelId, messages, options, task?, runner?, dtype?, device? }
12
+ * → generate { id, modelId, messages, options, tools?, toolChoice?, task?, runner?, dtype?, device? }
13
13
  * → cancel { id }
14
14
  * → command { id, modelId, name, payload, task?, runner?, dtype?, device? }
15
15
  * ← progress { id, status, file?, progress?, detail? }
@@ -26,7 +26,7 @@
26
26
  * kept — a local model is gigabytes, and two of them resident is the failure to avoid.
27
27
  */
28
28
  import type { AparteChatMessage, AparteStreamEvent, AparteUsage } from '@aparte/core';
29
- import type { BuiltInRunner, Device, Dtype, GenerationOptions, RunnerModule, RunnerProgress, TransformersModule } from './runners/types.js';
29
+ import type { BuiltInRunner, Device, Dtype, GenerationOptions, RunnerGenerateInput, RunnerModule, RunnerProgress, RunnerTool, TransformersModule } from './runners/types.js';
30
30
  interface RunnerSelection {
31
31
  modelId: string;
32
32
  task?: BuiltInRunner;
@@ -45,6 +45,8 @@ export type InMessage = {
45
45
  id: string;
46
46
  messages: AparteChatMessage[];
47
47
  options: GenerationOptions;
48
+ tools?: RunnerTool[];
49
+ toolChoice?: RunnerGenerateInput['toolChoice'];
48
50
  } & RunnerSelection) | {
49
51
  type: 'cancel';
50
52
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"worker-host.d.ts","sourceRoot":"","sources":["../src/worker-host.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"worker-host.d.ts","sourceRoot":"","sources":["../src/worker-host.ts"],"names":[],"mappings":"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"}
package/dist/worker.js CHANGED
@@ -80,6 +80,8 @@ function createWorkerHost(deps) {
80
80
  await runner.generate({
81
81
  messages: msg.messages,
82
82
  options: msg.options,
83
+ ...msg.tools ? { tools: msg.tools } : {},
84
+ ...msg.toolChoice ? { toolChoice: msg.toolChoice } : {},
83
85
  signal: controller.signal,
84
86
  emit: (event) => {
85
87
  if (closed) return;
@@ -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 * Whether this worker can run on WebGPU: an adapter answers. Asked once, the first time a\n * model loads on `'auto'`. Defaults to `navigator.gpu.requestAdapter()`.\n */\n hasWebGpu?(): Promise<boolean>;\n}\n\n/** WebGPU is usable when the API exists AND an adapter answers — the API alone can be there with no GPU behind it. */\nasync function defaultHasWebGpu(): Promise<boolean> {\n const gpu = (globalThis.navigator as { gpu?: { requestAdapter(): Promise<unknown> } } | undefined)?.gpu;\n if (!gpu) return false;\n try { return (await gpu.requestAdapter()) != null; } catch { return false; }\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 let webGpu: Promise<boolean> | undefined;\n\n // `'auto'` is resolved here rather than passed on: Transformers.js would try WebNN before\n // WebGPU, and dropping it (as the runners used to) left its browser default, WASM.\n const resolveDevice = async (device: Device | undefined): Promise<Device | undefined> => {\n if (device !== 'auto') return device;\n webGpu ??= (deps.hasWebGpu ?? defaultHasWebGpu)();\n return (await webGpu) ? 'webgpu' : 'wasm';\n };\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 device = await resolveDevice(sel.device);\n const create = (on: Device | undefined): Promise<TransformersRunner> => createRunner({\n transformers,\n modelId: sel.modelId,\n dtype: sel.dtype,\n device: on,\n progress: (p) => { if (progressId) deps.post({ type: 'progress', id: progressId, ...p }); },\n warn,\n });\n let runner: TransformersRunner;\n try {\n runner = await create(device);\n } catch (err) {\n // An adapter that answers is not a GPU that runs every model: a blocklisted driver,\n // an operator the WebGPU backend lacks. When the page asked for `'auto'`, WASM is\n // still an answer, as it always was before `'auto'` meant WebGPU. A forced\n // `'webgpu'` fails as asked.\n if (sel.device !== 'auto' || device !== 'webgpu') throw err;\n warn(`[provider-transformers] ${sel.modelId} could not load on WebGPU (${errorText(err, 'unknown error')}); running it on WASM.`);\n runner = await create('wasm');\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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"}
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, tools?, toolChoice?, 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, RunnerGenerateInput, RunnerModule, RunnerProgress, RunnerTool, 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; tools?: RunnerTool[]; toolChoice?: RunnerGenerateInput['toolChoice'] } & 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 * Whether this worker can run on WebGPU: an adapter answers. Asked once, the first time a\n * model loads on `'auto'`. Defaults to `navigator.gpu.requestAdapter()`.\n */\n hasWebGpu?(): Promise<boolean>;\n}\n\n/** WebGPU is usable when the API exists AND an adapter answers — the API alone can be there with no GPU behind it. */\nasync function defaultHasWebGpu(): Promise<boolean> {\n const gpu = (globalThis.navigator as { gpu?: { requestAdapter(): Promise<unknown> } } | undefined)?.gpu;\n if (!gpu) return false;\n try { return (await gpu.requestAdapter()) != null; } catch { return false; }\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 let webGpu: Promise<boolean> | undefined;\n\n // `'auto'` is resolved here rather than passed on: Transformers.js would try WebNN before\n // WebGPU, and dropping it (as the runners used to) left its browser default, WASM.\n const resolveDevice = async (device: Device | undefined): Promise<Device | undefined> => {\n if (device !== 'auto') return device;\n webGpu ??= (deps.hasWebGpu ?? defaultHasWebGpu)();\n return (await webGpu) ? 'webgpu' : 'wasm';\n };\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 device = await resolveDevice(sel.device);\n const create = (on: Device | undefined): Promise<TransformersRunner> => createRunner({\n transformers,\n modelId: sel.modelId,\n dtype: sel.dtype,\n device: on,\n progress: (p) => { if (progressId) deps.post({ type: 'progress', id: progressId, ...p }); },\n warn,\n });\n let runner: TransformersRunner;\n try {\n runner = await create(device);\n } catch (err) {\n // An adapter that answers is not a GPU that runs every model: a blocklisted driver,\n // an operator the WebGPU backend lacks. When the page asked for `'auto'`, WASM is\n // still an answer, as it always was before `'auto'` meant WebGPU. A forced\n // `'webgpu'` fails as asked.\n if (sel.device !== 'auto' || device !== 'webgpu') throw err;\n warn(`[provider-transformers] ${sel.modelId} could not load on WebGPU (${errorText(err, 'unknown error')}); running it on WASM.`);\n runner = await create('wasm');\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 ...(msg.tools ? { tools: msg.tools } : {}),\n ...(msg.toolChoice ? { toolChoice: msg.toolChoice } : {}),\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":"AAsEA,eAAe,mBAAqC;AAChD,QAAM,MAAO,WAAW,WAA4E;AACpG,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AAAE,WAAQ,MAAM,IAAI,eAAA,KAAqB;AAAA,EAAM,QAAQ;AAAE,WAAO;AAAA,EAAO;AAC/E;AAEA,MAAM,YAAY,CAAC,KAAc,aAA8B,eAAe,SAAS,IAAI,WAAY;AAEhG,SAAS,iBAAiB,MAA2D;AACxF,MAAI;AACJ,MAAI,UAA8D;AAClE,QAAM,gCAAgB,IAAA;AACtB,QAAM,6BAAa,IAAA;AACnB,MAAI;AAIJ,QAAM,gBAAgB,OAAO,WAA4D;AACrF,QAAI,WAAW,OAAQ,QAAO;AAC9B,gBAAY,KAAK,aAAa,kBAAA;AAC9B,WAAQ,MAAM,SAAU,WAAW;AAAA,EACvC;AAEA,QAAM,OAAO,CAAC,YAA0B;AACpC,QAAI,OAAO,IAAI,OAAO,EAAG;AACzB,WAAO,IAAI,OAAO;AAClB,SAAK,KAAK,EAAE,MAAM,WAAW,SAAS;AAAA,EAC1C;AAEA,iBAAe,aAAa,KAAsB,YAAkD;AAChG,UAAM,OAAO,IAAI,QAAQ;AACzB,UAAM,MAAM,GAAG,IAAI,OAAO,KAAK,IAAI,UAAU,IAAI;AACjD,QAAI,SAAS,QAAQ,IAAK,QAAO,QAAQ;AACzC,QAAI,SAAS;AACT,YAAM,WAAW;AACjB,gBAAU;AACV,YAAM,SAAS,OAAO,UAAA;AAAA,IAC1B;AACA,UAAM,eAAe,MAAM,KAAK,iBAAiB,SAAS;AAC1D,UAAM,EAAE,iBAAiB,MAAM,KAAK,aAAa,EAAE,MAAM,QAAQ,IAAI,QAAQ;AAC7E,UAAM,SAAS,MAAM,cAAc,IAAI,MAAM;AAC7C,UAAM,SAAS,CAAC,OAAwD,aAAa;AAAA,MACjF;AAAA,MACA,SAAS,IAAI;AAAA,MACb,OAAO,IAAI;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,CAAC,MAAM;AAAE,YAAI,WAAY,MAAK,KAAK,EAAE,MAAM,YAAY,IAAI,YAAY,GAAG,GAAG;AAAA,MAAG;AAAA,MAC1F;AAAA,IAAA,CACH;AACD,QAAI;AACJ,QAAI;AACA,eAAS,MAAM,OAAO,MAAM;AAAA,IAChC,SAAS,KAAK;AAKV,UAAI,IAAI,WAAW,UAAU,WAAW,SAAU,OAAM;AACxD,WAAK,2BAA2B,IAAI,OAAO,8BAA8B,UAAU,KAAK,eAAe,CAAC,wBAAwB;AAChI,eAAS,MAAM,OAAO,MAAM;AAAA,IAChC;AACA,cAAU,EAAE,KAAK,OAAA;AACjB,SAAK,KAAK,EAAE,MAAM,kBAAkB,SAAS,IAAI,SAAS;AAC1D,WAAO;AAAA,EACX;AAEA,iBAAe,cAAc,KAA6D;AACtF,QAAI;AACA,YAAM,aAAa,KAAK,IAAI,EAAE;AAC9B,WAAK,KAAK,EAAE,MAAM,YAAY,IAAI,IAAI,IAAI,QAAQ,SAAS;AAAA,IAC/D,SAAS,KAAK;AACV,WAAK,KAAK,EAAE,MAAM,iBAAiB,IAAI,IAAI,IAAI,SAAS,UAAU,KAAK,sBAAsB,EAAA,CAAG;AAAA,IACpG;AAAA,EACJ;AAEA,iBAAe,eAAe,KAA8D;AAIxF,UAAM,aAAa,UAAU,IAAI,IAAI,EAAE,KAAK,IAAI,gBAAA;AAChD,cAAU,IAAI,IAAI,IAAI,UAAU;AAChC,QAAI;AACJ,QAAI,SAAS;AACb,QAAI;AACA,UAAI,WAAW,OAAO,SAAS;AAI3B,aAAK,KAAK,EAAE,MAAM,YAAY,IAAI,IAAI,IAAI;AAC1C;AAAA,MACJ;AACA,YAAM,SAAS,MAAM,aAAa,KAAK,IAAI,EAAE;AAC7C,YAAM,OAAO,SAAS;AAAA,QAClB,UAAU,IAAI;AAAA,QACd,SAAS,IAAI;AAAA,QACb,GAAI,IAAI,QAAQ,EAAE,OAAO,IAAI,MAAA,IAAU,CAAA;AAAA,QACvC,GAAI,IAAI,aAAa,EAAE,YAAY,IAAI,WAAA,IAAe,CAAA;AAAA,QACtD,QAAQ,WAAW;AAAA,QACnB,MAAM,CAAC,UAAU;AACb,cAAI,OAAQ;AAGZ,cAAI,MAAM,SAAS,QAAQ;AAAE,oBAAQ,MAAM;AAAO;AAAA,UAAQ;AAC1D,cAAI,MAAM,SAAS,SAAS;AAAE,qBAAS;AAAM,iBAAK,KAAK,EAAE,MAAM,aAAa,IAAI,IAAI,IAAI,SAAS,MAAM,QAAA,CAAS;AAAG;AAAA,UAAQ;AAC3H,eAAK,KAAK,EAAE,MAAM,aAAa,IAAI,IAAI,IAAI,OAAO;AAAA,QACtD;AAAA,MAAA,CACH;AACD,UAAI,CAAC,OAAQ,MAAK,KAAK,EAAE,MAAM,YAAY,IAAI,IAAI,IAAI,GAAI,QAAQ,EAAE,UAAU,CAAA,GAAK;AAAA,IACxF,SAAS,KAAK;AACV,UAAI,CAAC,OAAQ,MAAK,KAAK,EAAE,MAAM,aAAa,IAAI,IAAI,IAAI,SAAS,UAAU,KAAK,mBAAmB,GAAG;AAAA,IAC1G,UAAA;AACI,gBAAU,OAAO,IAAI,EAAE;AAAA,IAC3B;AAAA,EACJ;AAEA,iBAAe,cAAc,KAA6D;AACtF,QAAI;AACA,YAAM,SAAS,MAAM,aAAa,GAAG;AACrC,UAAI,CAAC,OAAO,SAAS;AACjB,aAAK,KAAK,EAAE,MAAM,kBAAkB,IAAI,IAAI,IAAI,OAAO,kDAAkD,IAAI,IAAI,KAAA,CAAM;AACvH;AAAA,MACJ;AACA,YAAM,SAAS,MAAM,OAAO,QAAQ,IAAI,MAAM,IAAI,OAAO;AACzD,WAAK,KAAK,EAAE,MAAM,kBAAkB,IAAI,IAAI,IAAI,QAAQ;AAAA,IAC5D,SAAS,KAAK;AACV,WAAK,KAAK,EAAE,MAAM,kBAAkB,IAAI,IAAI,IAAI,OAAO,UAAU,KAAK,gBAAgB,EAAA,CAAG;AAAA,IAC7F;AAAA,EACJ;AAoBA,MAAI,QAAuB,QAAQ,QAAA;AACnC,QAAM,YAAY,CAAC,SAAoC;AACnD,YAAQ,MAAM,KAAK,IAAI,EAAE,MAAM,MAAM;AAAA,IAA2C,CAAC;AAAA,EACrF;AAEA,SAAO;AAAA,IACH,UAAU,KAAsB;AAC5B,cAAQ,IAAI,MAAA;AAAA,QACR,KAAK;AAAQ,sBAAY,IAAI;AAAiB;AAAA,QAC9C,KAAK;AAAW,oBAAU,MAAM,cAAc,GAAG,CAAC;AAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QASrD,KAAK;AAAY,oBAAU,IAAI,IAAI,IAAI,IAAI,iBAAiB;AAAG,oBAAU,MAAM,eAAe,GAAG,CAAC;AAAG;AAAA,QACrG,KAAK;AAAU,oBAAU,IAAI,IAAI,EAAE,GAAG,MAAA;AAAS;AAAA,QAC/C,KAAK;AAAW,oBAAU,MAAM,cAAc,GAAG,CAAC;AAAG;AAAA,MAAA;AAAA,IAE7D;AAAA,EAAA;AAER;ACpNA,IAAI,MAA0C;AAE9C,SAAS,iBAAiB,WAAiD;AACvE,WAAS,YAAY;AACjB,QAAI;AACJ,QAAI;AACA,YAAM,MAAM,OAAO,2BAA2B;AAAA,IAClD,SAAS,mBAAmB;AACxB,UAAI,CAAC,UAAW,OAAM;AACtB,YAAM,MAAM;AAAA;AAAA,QAA0B;AAAA;AAAA,IAC1C;AAGA,QAAI,IAAI,mBAAmB;AAC3B,QAAI,IAAI,kBAAkB;AAC1B,WAAO;AAAA,EACX,GAAA;AACA,SAAO;AACX;AASA,MAAM,WAA+D;AAAA,EACjE,mBAAmB,MAAM,OAAO,8BAA8B;AAAA,EAC9D,sBAAsB,MAAM,OAAO,iCAAiC;AACxE;AAEA,SAAS,aAAa,EAAE,MAAM,UAA2E;AAGrG,MAAI,OAAQ,QAAO;AAAA;AAAA,IAA0B;AAAA;AAC7C,SAAO,SAAS,IAAI,EAAA;AACxB;AAKA,MAAM,MAAM;AAEZ,MAAM,OAAO,iBAAiB;AAAA,EAC1B,MAAM,CAAC,YAAY;AAAE,QAAI,YAAY,OAAO;AAAA,EAAG;AAAA,EAC/C;AAAA,EACA;AACJ,CAAC;AAED,IAAI,iBAAiB,WAAW,CAAC,UAAwB;AAAE,OAAK,UAAU,MAAM,IAAI;AAAG,CAAC;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aparte/provider-transformers",
3
- "version": "0.24.0",
3
+ "version": "0.26.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.24.0 <1.0.0",
28
+ "@aparte/core": ">=0.26.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.24.0"
36
+ "@aparte/core": "0.26.0"
37
37
  },
38
38
  "keywords": [
39
39
  "ai",