@aparte/provider-transformers 0.19.3 → 0.20.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 +13 -7
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -31
- package/dist/index.js.map +1 -1
- package/dist/runners/image-text-to-text.d.ts.map +1 -1
- package/dist/runners/image-text-to-text.js +3 -2
- package/dist/runners/image-text-to-text.js.map +1 -1
- package/dist/runners/shared.d.ts +25 -1
- package/dist/runners/shared.d.ts.map +1 -1
- package/dist/runners/shared.js +17 -1
- package/dist/runners/shared.js.map +1 -1
- package/dist/runners/text-generation.d.ts.map +1 -1
- package/dist/runners/text-generation.js +3 -2
- package/dist/runners/text-generation.js.map +1 -1
- package/dist/runners/types.d.ts +4 -0
- package/dist/runners/types.d.ts.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -77,11 +77,12 @@ registerModel({ id: 'my-org/my-model', name: 'Mine', capabilities: ['streaming']
|
|
|
77
77
|
// runners/mine.js — served by your app; the worker imports it by URL
|
|
78
78
|
export async function createRunner(ctx) {
|
|
79
79
|
// ctx.transformers is the Transformers.js the page installed or mapped — never import your own
|
|
80
|
-
const { AutoTokenizer, AutoModelForCausalLM, TextStreamer } = ctx.transformers;
|
|
80
|
+
const { AutoTokenizer, AutoModelForCausalLM, TextStreamer, InterruptableStoppingCriteria } = ctx.transformers;
|
|
81
81
|
const tokenizer = await AutoTokenizer.from_pretrained(ctx.modelId);
|
|
82
82
|
const model = await AutoModelForCausalLM.from_pretrained(ctx.modelId, { dtype: ctx.dtype, device: ctx.device });
|
|
83
83
|
return {
|
|
84
|
-
async generate({ messages, options, emit }) {
|
|
84
|
+
async generate({ messages, options, emit, signal }) {
|
|
85
|
+
if (signal.aborted) return;
|
|
85
86
|
// messages arrive WITH their content parts — render them the way this model wants;
|
|
86
87
|
// this one reads text, so the parts are flattened and tool turns left out
|
|
87
88
|
const turns = messages
|
|
@@ -92,7 +93,10 @@ export async function createRunner(ctx) {
|
|
|
92
93
|
skip_prompt: true, skip_special_tokens: true,
|
|
93
94
|
callback_function: (text) => emit({ type: 'text', delta: text }),
|
|
94
95
|
});
|
|
95
|
-
|
|
96
|
+
// Stop: without this the model generates on to its ceiling, and the next reply waits behind it
|
|
97
|
+
const stopping = new InterruptableStoppingCriteria();
|
|
98
|
+
signal.addEventListener('abort', () => stopping.interrupt(), { once: true });
|
|
99
|
+
await model.generate({ ...inputs, max_new_tokens: options.maxTokens ?? 8192, streamer, stopping_criteria: stopping });
|
|
96
100
|
// `done` is optional — the provider closes the stream when generate() resolves
|
|
97
101
|
},
|
|
98
102
|
// anything that is not a generation: the provider passes it through, untouched
|
|
@@ -121,14 +125,16 @@ one WebGPU pipeline, so a worker per chat would mean N copies resident in one ta
|
|
|
121
125
|
the **same** model is the case this is for: they share the load, for free.
|
|
122
126
|
|
|
123
127
|
Two chats on **different** models is the case that costs. They serialize on the one pipeline,
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
so each turn unloads one model and loads the other. The cache budget adds to that: it is how
|
|
129
|
+
many models stay **downloaded** in the browser's Cache API (disk, not memory: one model is
|
|
130
|
+
loaded at a time whatever the budget), and at the default of one, a switch can delete the other
|
|
131
|
+
model's weights and download them again. The provider warns once when it sees it:
|
|
126
132
|
|
|
127
133
|
```ts
|
|
128
134
|
import { setMaxCachedModels, getMaxCachedModels } from '@aparte/provider-transformers';
|
|
129
135
|
|
|
130
|
-
setMaxCachedModels(2); // keep both
|
|
131
|
-
setMaxCachedModels(0); // no limit —
|
|
136
|
+
setMaxCachedModels(2); // keep both downloaded, if the browser's storage has room
|
|
137
|
+
setMaxCachedModels(0); // no limit — the browser's storage quota is the only bound
|
|
132
138
|
getMaxCachedModels(); // the current budget
|
|
133
139
|
```
|
|
134
140
|
|
package/dist/index.d.ts
CHANGED
|
@@ -66,6 +66,14 @@ export interface TransformersModelConfig {
|
|
|
66
66
|
* the same Transformers.js instance the built-ins use. Wins over `task`.
|
|
67
67
|
*/
|
|
68
68
|
runner?: string;
|
|
69
|
+
/**
|
|
70
|
+
* The most tokens a reply from this model may run to, unless the request sets its own
|
|
71
|
+
* `maxTokens`. Left unset, a built-in runner stops at 8192. With a built-in runner, whatever
|
|
72
|
+
* the ceiling, a reply also ends where the model's context window does, prompt included, and
|
|
73
|
+
* Stop interrupts it. A runner of your own receives `options.maxTokens` only when the request
|
|
74
|
+
* or this setting gives one, so it sets its own default (the README's example uses 8192).
|
|
75
|
+
*/
|
|
76
|
+
maxTokens?: number;
|
|
69
77
|
/** ONNX dtype or per-part dtype map (e.g. `'q4'` or `{ decoder_model_merged: 'q4' }`). */
|
|
70
78
|
dtype?: Dtype;
|
|
71
79
|
/** Not read: where models run is one setting for the page, {@link setComputeDevice}. */
|
|
@@ -77,8 +85,13 @@ export interface TransformersModelConfig {
|
|
|
77
85
|
*/
|
|
78
86
|
export declare function registerModel(config: TransformersModelConfig): void;
|
|
79
87
|
/**
|
|
80
|
-
* Set the maximum number of models to keep in
|
|
81
|
-
* model is ready, the oldest models are
|
|
88
|
+
* Set the maximum number of models to keep downloaded in the browser's Cache API. When
|
|
89
|
+
* exceeded after a new model is ready, the oldest models' weights are deleted. 0 = unlimited.
|
|
90
|
+
* It bounds disk, not memory: one model is loaded at a time whatever the budget.
|
|
91
|
+
*
|
|
92
|
+
* The budget counts and evicts only the models given to {@link registerModel} (and the
|
|
93
|
+
* one just loaded). Other Transformers.js weights in the origin's cache, from your own
|
|
94
|
+
* pipelines or another library, are never counted and never deleted.
|
|
82
95
|
*/
|
|
83
96
|
export declare function setMaxCachedModels(max: number): void;
|
|
84
97
|
/** Returns the current max-cached-models setting. */
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,aAAa,EAKhB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAOvE,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC7B,kBAAkB,EAAE,MAAM,CAAC;CAC9B;AAKD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAE9F;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,eAAe,CAAC,CA8B/D;AAMD,8DAA8D;AAC9D,MAAM,WAAW,uBAAuB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IAC5C;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,wFAAwF;IACxF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAQD;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAUnE;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -38,12 +38,6 @@ function registerModel(config) {
|
|
|
38
38
|
}];
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
function _modelFromCacheEntry(modelId) {
|
|
42
|
-
const config = _registeredModels.get(modelId);
|
|
43
|
-
if (config) return { id: config.id, name: config.name, description: config.description, capabilities: config.capabilities };
|
|
44
|
-
const name = (modelId.split("/").pop() ?? modelId).replace(/-/g, " ");
|
|
45
|
-
return { id: modelId, name, capabilities: ["streaming"] };
|
|
46
|
-
}
|
|
47
41
|
let _maxCachedModels = 1;
|
|
48
42
|
function setMaxCachedModels(max) {
|
|
49
43
|
_maxCachedModels = max;
|
|
@@ -61,7 +55,7 @@ function getComputeDevice() {
|
|
|
61
55
|
async function _enforceMaxCachedModels(keepModelId) {
|
|
62
56
|
if (_maxCachedModels === 0) return;
|
|
63
57
|
try {
|
|
64
|
-
const cached = await listCachedModels();
|
|
58
|
+
const cached = (await listCachedModels()).filter((e) => _registeredModels.has(e.modelId) || e.modelId === keepModelId);
|
|
65
59
|
const inUse = new Set(_queuedModelIds.values());
|
|
66
60
|
const others = cached.filter((e) => e.modelId !== keepModelId && !inUse.has(e.modelId));
|
|
67
61
|
const excess = cached.length - _maxCachedModels;
|
|
@@ -72,17 +66,6 @@ async function _enforceMaxCachedModels(keepModelId) {
|
|
|
72
66
|
} catch {
|
|
73
67
|
}
|
|
74
68
|
}
|
|
75
|
-
async function _refreshKnownModels() {
|
|
76
|
-
try {
|
|
77
|
-
const cached = await listCachedModels();
|
|
78
|
-
for (const entry of cached) {
|
|
79
|
-
if (!_knownModels.find((m) => m.id === entry.modelId)) {
|
|
80
|
-
_knownModels = [..._knownModels, _modelFromCacheEntry(entry.modelId)];
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
} catch {
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
69
|
function _selection(modelId) {
|
|
87
70
|
const config = _registeredModels.get(modelId);
|
|
88
71
|
const runner = config?.runner;
|
|
@@ -110,8 +93,9 @@ function _warnIfContended(requested) {
|
|
|
110
93
|
const other = _contendingModelId(requested);
|
|
111
94
|
if (!other) return;
|
|
112
95
|
_warnedModelContention = true;
|
|
96
|
+
const deletes = _maxCachedModels === 1 ? ` With a cache budget of 1, a switch also deletes the other model's downloaded weights and downloads them again; setMaxCachedModels(2) keeps both, if the browser's storage has room.` : "";
|
|
113
97
|
console.warn(
|
|
114
|
-
`[Aparte] Two chats are driving different local models at once ("${requested}" behind "${other}"). Transformers.js runs one pipeline per tab, so these generates are serialized
|
|
98
|
+
`[Aparte] Two chats are driving different local models at once ("${requested}" behind "${other}"). Transformers.js runs one pipeline per tab, so these generates are serialized: each switch unloads one model and loads the other.${deletes} Point both chats at one model to avoid it. This warns once.`
|
|
115
99
|
);
|
|
116
100
|
}
|
|
117
101
|
function _releaseGenerateSlot(id) {
|
|
@@ -245,7 +229,7 @@ function _handleWorkerMessage(event) {
|
|
|
245
229
|
case "pipeline-ready": {
|
|
246
230
|
_loadedModelId = msg.modelId;
|
|
247
231
|
_preparingModelId = null;
|
|
248
|
-
void _enforceMaxCachedModels(msg.modelId)
|
|
232
|
+
void _enforceMaxCachedModels(msg.modelId);
|
|
249
233
|
break;
|
|
250
234
|
}
|
|
251
235
|
case "gen-event": {
|
|
@@ -304,14 +288,18 @@ const TransformersProvider = {
|
|
|
304
288
|
getModels() {
|
|
305
289
|
return _knownModels;
|
|
306
290
|
},
|
|
291
|
+
/**
|
|
292
|
+
* The registered models. Only those: the Cache API is the origin's, and listing what it
|
|
293
|
+
* held offered an app's own embedder, or another library's model, as a chat model to
|
|
294
|
+
* pick. `listCachedModels()` still reports everything downloaded, for a settings panel.
|
|
295
|
+
*/
|
|
307
296
|
async fetchModels() {
|
|
308
|
-
await _refreshKnownModels();
|
|
309
297
|
return _knownModels;
|
|
310
298
|
},
|
|
311
299
|
async chat(request, _config, ctx) {
|
|
312
300
|
const requestId = uuid();
|
|
313
301
|
const options = {
|
|
314
|
-
maxTokens: request.maxTokens,
|
|
302
|
+
maxTokens: request.maxTokens ?? _registeredModels.get(request.modelId)?.maxTokens,
|
|
315
303
|
temperature: request.temperature,
|
|
316
304
|
seed: request.seed
|
|
317
305
|
};
|
|
@@ -401,12 +389,11 @@ const TransformersProvider = {
|
|
|
401
389
|
if (_preparingModelId === modelId) return "cached";
|
|
402
390
|
if ("caches" in globalThis) {
|
|
403
391
|
try {
|
|
404
|
-
const encodedId = encodeURIComponent(modelId);
|
|
405
392
|
const names = await caches.keys();
|
|
406
393
|
for (const name of names) {
|
|
407
394
|
const cache = await caches.open(name);
|
|
408
395
|
const keys = await cache.keys();
|
|
409
|
-
if (keys.some((r) => r.url
|
|
396
|
+
if (keys.some((r) => _modelIdOfUrl(r.url) === modelId)) {
|
|
410
397
|
return "cached";
|
|
411
398
|
}
|
|
412
399
|
}
|
|
@@ -479,13 +466,13 @@ function terminateWorker() {
|
|
|
479
466
|
_queuedModelIds.clear();
|
|
480
467
|
_warnedModelContention = false;
|
|
481
468
|
}
|
|
469
|
+
function _modelIdOfUrl(url) {
|
|
470
|
+
const m = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\//);
|
|
471
|
+
return m ? decodeURIComponent(m[1]) : null;
|
|
472
|
+
}
|
|
482
473
|
async function listCachedModels() {
|
|
483
474
|
if (!("caches" in globalThis)) return [];
|
|
484
475
|
const found = /* @__PURE__ */ new Map();
|
|
485
|
-
function extractModelId(url) {
|
|
486
|
-
const m = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\//);
|
|
487
|
-
return m ? decodeURIComponent(m[1]) : null;
|
|
488
|
-
}
|
|
489
476
|
function modelName(modelId) {
|
|
490
477
|
const config = _registeredModels.get(modelId);
|
|
491
478
|
if (config) return config.name;
|
|
@@ -498,7 +485,7 @@ async function listCachedModels() {
|
|
|
498
485
|
const cache = await caches.open(cacheName);
|
|
499
486
|
const requests = await cache.keys();
|
|
500
487
|
for (const req of requests) {
|
|
501
|
-
const modelId =
|
|
488
|
+
const modelId = _modelIdOfUrl(req.url);
|
|
502
489
|
if (!modelId) continue;
|
|
503
490
|
if (!found.has(modelId)) {
|
|
504
491
|
found.set(modelId, { name: modelName(modelId), sizeBytes: 0 });
|
|
@@ -540,9 +527,8 @@ async function deleteCachedModel(modelId) {
|
|
|
540
527
|
try {
|
|
541
528
|
const cache = await caches.open(cacheName);
|
|
542
529
|
const requests = await cache.keys();
|
|
543
|
-
const encoded = encodeURIComponent(modelId);
|
|
544
530
|
await Promise.all(
|
|
545
|
-
requests.filter((r) => r.url
|
|
531
|
+
requests.filter((r) => _modelIdOfUrl(r.url) === modelId).map((r) => cache.delete(r))
|
|
546
532
|
);
|
|
547
533
|
} catch {
|
|
548
534
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @aparte/provider-transformers — run LLMs 100% in the browser via Transformers.js.\n *\n * A local, keyless `AparteAIProvider`: it owns its I/O (inference runs off the main\n * thread in a Web Worker) so `AparteDirectTransport` delegates to its `chat()`. Model\n * weights download once and persist in the Cache API.\n *\n * Scope: the worker runs a **runner** — the built-in `text-generation` (any chat model\n * behind Transformers.js' `pipeline()`), or a module of the app's own named by\n * `TransformersModelConfig.runner` (see `runners/types.ts` for the contract). Tool-calling\n * for local models is model-specific (every family has its own wire format), so the\n * built-in drops tool turns and says so; a custom runner may render them.\n *\n * ## This provider's state is TAB-scoped, on purpose\n *\n * Everything below the \"Worker bridge\" heading — the worker, the loaded model, the\n * generate chain — plus `setComputeDevice`, `setMaxCachedModels` and\n * `setHardwareTierModels`, is module-level and therefore shared by every chat on the\n * page. That is deliberate, and it is the opposite of what the rest of the suite does:\n * a plugin's providers scope to one chat, this one cannot.\n *\n * The reason is the resource, not the design. A local model is 1–2 GB of weights and one\n * WebGPU pipeline. Handing each chat its own worker would mean N copies resident in one\n * tab — which is the failure this package exists to avoid, not a capability. The\n * settings above describe the *machine* (which backend, how many models to keep\n * cached), so per-chat values would not mean anything either.\n *\n * What the constraint costs: two chats on the page driving DIFFERENT local models take\n * turns on one pipeline, so each turn may evict and reload gigabytes. That used to\n * happen silently — a multi-second stall with nothing to read. It now warns once, from\n * `chat()`, when a generate is queued for a model other than the one already in flight.\n * Same model in both chats is free and correct: they share the load.\n */\n\nimport type {\n AparteAIProvider,\n AparteAIModel,\n AparteChatRequest,\n AparteChatResponse,\n ModelStatus,\n ModelLoadProgress,\n} from '@aparte/core';\nimport { uuid } from '@aparte/core';\nimport type { BuiltInRunner, Device, Dtype } from './runners/types.js';\n\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Hardware detection\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface HardwareProfile {\n hasGpu: boolean;\n ramGb: number;\n tier: 'low' | 'mid' | 'high';\n recommendedModelId: string;\n}\n\n/** Hardware-tier model overrides — set by the app via setHardwareTierModels(). */\nlet _hardwareTiers: { low: string; mid?: string; high: string } | null = null;\n\n/**\n * Set the model IDs to use per hardware tier. Call before detectHardware() is used\n * to pick a default model — the provider ships no model knowledge of its own.\n */\nexport function setHardwareTierModels(tiers: { low: string; mid?: string; high: string }): void {\n _hardwareTiers = tiers;\n}\n\nexport async function detectHardware(): Promise<HardwareProfile> {\n // navigator.deviceMemory: W3C API, Chromium only, capped at 8 GB for privacy\n // (1 | 2 | 4 | 8). Falls back to 4 on Firefox/Safari.\n const ramGb: number = (navigator as unknown as { deviceMemory?: number }).deviceMemory ?? 4;\n\n // Real WebGPU check: requestAdapter() returns null if no capable GPU is present.\n let hasGpu = false;\n if ('gpu' in navigator) {\n try {\n const adapter = await (navigator as unknown as { gpu: { requestAdapter(): Promise<unknown> } }).gpu.requestAdapter();\n hasGpu = adapter !== null;\n } catch {\n hasGpu = false;\n }\n }\n\n let tier: 'low' | 'mid' | 'high';\n if (!hasGpu || ramGb < 4) {\n tier = 'low';\n } else if (ramGb < 8) {\n tier = 'mid';\n } else {\n tier = 'high';\n }\n\n const recommendedModelId = _hardwareTiers\n ? (_hardwareTiers[tier] ?? _hardwareTiers.high ?? '')\n : '';\n\n return { hasGpu, ramGb, tier, recommendedModelId };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Model catalog — all model knowledge lives in the app, not the provider.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Configuration for a model registered with the provider. */\nexport interface TransformersModelConfig {\n id: string;\n name: string;\n description?: string;\n capabilities: AparteAIModel['capabilities'];\n /**\n * Which built-in runner loads and drives the model. `'text-generation'` (the default)\n * is any chat model behind Transformers.js' `pipeline()`. Ignored when `runner` is set.\n */\n task?: BuiltInRunner;\n /**\n * A runner of your own: the URL of an ES module exporting `createRunner` (see\n * `TransformersRunner`). Resolved against the page, imported by the worker, and handed\n * the same Transformers.js instance the built-ins use. Wins over `task`.\n */\n runner?: string;\n /** ONNX dtype or per-part dtype map (e.g. `'q4'` or `{ decoder_model_merged: 'q4' }`). */\n dtype?: Dtype;\n /** 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() and cache discovery. */\nlet _knownModels: AparteAIModel[] = [];\n\n/**\n * Register a model with the provider. Call before the model is used for inference.\n */\nexport function registerModel(config: TransformersModelConfig): void {\n _registeredModels.set(config.id, config);\n if (!_knownModels.find(m => m.id === config.id)) {\n _knownModels = [..._knownModels, {\n id: config.id,\n name: config.name,\n description: config.description,\n capabilities: config.capabilities,\n }];\n }\n}\n\n/** Build an AparteAIModel entry from a cache-discovered modelId not in the registry. */\nfunction _modelFromCacheEntry(modelId: string): AparteAIModel {\n const config = _registeredModels.get(modelId);\n if (config) return { id: config.id, name: config.name, description: config.description, capabilities: config.capabilities };\n const name = (modelId.split('/').pop() ?? modelId).replace(/-/g, ' ');\n return { id: modelId, name, capabilities: ['streaming'] };\n}\n\n/** Max number of models to keep in cache. 0 = unlimited. Default: 1. */\nlet _maxCachedModels = 1;\n\n/**\n * Set the maximum number of models to keep in cache. When exceeded after a new\n * model is ready, the oldest models are evicted. 0 = unlimited.\n */\nexport function setMaxCachedModels(max: number): void {\n _maxCachedModels = max;\n}\n\n/** Returns the current max-cached-models setting. */\nexport function getMaxCachedModels(): number {\n return _maxCachedModels;\n}\n\n/**\n * User's preferred compute backend for local inference.\n * 'auto' → WebGPU when available, else WASM (default)\n * 'webgpu' → force WebGPU\n * 'wasm' → force WASM CPU\n */\nexport type ComputeDevice = 'auto' | 'webgpu' | 'wasm';\nlet _computeDevice: ComputeDevice = 'auto';\n\nexport function setComputeDevice(d: ComputeDevice): void {\n _computeDevice = d;\n}\n\nexport function getComputeDevice(): ComputeDevice {\n return _computeDevice;\n}\n\n/**\n * Evict models from cache until count <= _maxCachedModels. `keepModelId` is never\n * evicted, and neither is a model a generate is queued or running on: the budget\n * fires from `pipeline-ready`, i.e. when ANOTHER model finishes loading, so it\n * used to delete the weights of the model that was answering — and\n * `deleteCachedModel` terminates the worker when that model is the loaded one.\n */\nasync function _enforceMaxCachedModels(keepModelId: string): Promise<void> {\n if (_maxCachedModels === 0) return; // unlimited\n try {\n const cached = await listCachedModels();\n const inUse = new Set(_queuedModelIds.values());\n const others = cached.filter(e => e.modelId !== keepModelId && !inUse.has(e.modelId));\n const excess = cached.length - _maxCachedModels;\n if (excess <= 0) return;\n // Delete the excess models (oldest first — they appear first in cache scan order).\n for (let i = 0; i < excess && i < others.length; i++) {\n await deleteCachedModel(others[i]!.modelId);\n }\n } catch { /* cache unavailable */ }\n}\n\n/** Merge cached models into _knownModels (idempotent). Called by fetchModels(). */\nasync function _refreshKnownModels(): Promise<void> {\n try {\n const cached = await listCachedModels();\n for (const entry of cached) {\n if (!_knownModels.find(m => m.id === entry.modelId)) {\n _knownModels = [..._knownModels, _modelFromCacheEntry(entry.modelId)];\n }\n }\n } catch { /* cache unavailable */ }\n}\n\n/**\n * How the worker should load `modelId`: which runner, which weights, which device.\n *\n * A custom `runner` is made absolute HERE, not in the worker: a worker's base URL is its\n * own script's, not the page's, and the blob shim `_spawnWorker` may build has no\n * meaningful base at all — so a relative path would resolve against the wrong place or\n * fail outright. The page is the one place that knows what the app meant.\n */\nfunction _selection(modelId: string): { task: BuiltInRunner; runner?: string; dtype?: Dtype; device: ComputeDevice } {\n const config = _registeredModels.get(modelId);\n const runner = config?.runner;\n return {\n task: config?.task ?? 'text-generation',\n ...(runner ? { runner: typeof location === 'undefined' ? runner : new URL(runner, location.href).href } : {}),\n dtype: config?.dtype,\n device: _computeDevice,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Worker bridge\n// ─────────────────────────────────────────────────────────────────────────────\n\nlet _worker: Worker | null = null;\n\ninterface PendingPrepare {\n modelId: string;\n onProgress: (p: ModelLoadProgress) => void;\n resolve: () => void;\n reject: (err: Error) => void;\n}\nconst _pendingPrepares = new Map<string, PendingPrepare>();\nconst _pendingGenerates = new Map<string, ReadableStreamDefaultController>();\nconst _pendingCommands = new Map<string, { resolve: (value: unknown) => void; reject: (err: Error) => void }>();\n\n// ── Generate serialization ──────────────────────────────────────────────────\n// The worker holds ONE pipeline: two concurrent generates would corrupt each\n// other. Each chat() chains its `generate` behind the previous generate's\n// completion (gen-done / gen-error).\nlet _generateChain: Promise<void> = Promise.resolve();\nconst _generateDoneResolvers = new Map<string, () => void>();\n\n// ── Contention on the one pipeline ──────────────────────────────────────────\n// Serialization is correct but invisible: two chats driving DIFFERENT local\n// models take turns, and with `maxCachedModels` at its default of 1 each turn\n// can evict and reload gigabytes. The user sees a stall; the developer sees\n// nothing. These two track just enough to say so, once.\nconst _queuedModelIds = new Map<string, string>();\nlet _warnedModelContention = false;\n\n/** Model ids of generates currently queued or running on the single pipeline. */\nfunction _contendingModelId(requested: string): string | undefined {\n for (const id of _queuedModelIds.values()) if (id !== requested) return id;\n return undefined;\n}\n\n/**\n * Warn once when a generate has to queue behind another chat's DIFFERENT model.\n * Not a warning about switching models in one chat — that is a deliberate act\n * with visible feedback. This fires only when two are in flight at once.\n */\nfunction _warnIfContended(requested: string): void {\n if (_warnedModelContention) return;\n const other = _contendingModelId(requested);\n if (!other) return;\n _warnedModelContention = true;\n console.warn(\n `[Aparte] Two chats are driving different local models at once (\"${requested}\" behind `\n + `\"${other}\"). Transformers.js runs one pipeline per tab, so these generates are `\n + `serialized, and with a cache budget of ${_maxCachedModels} each switch can evict and `\n + `reload gigabytes of weights. Point both chats at one model, or raise the budget with `\n + `setMaxCachedModels(2) if the machine has the memory. This warns once.`,\n );\n}\n\n/** Settle the serialization slot for a finished generate. */\nfunction _releaseGenerateSlot(id: string): void {\n _queuedModelIds.delete(id);\n const resolve = _generateDoneResolvers.get(id);\n if (resolve) {\n _generateDoneResolvers.delete(id);\n resolve();\n }\n}\n\n/** Model known to be loaded (main-thread view). */\nlet _loadedModelId: string | null = null;\n/** Model currently being prepared (for the getModelStatus 'cached' path). */\nlet _preparingModelId: string | null = null;\n\n/**\n * The worker this package publishes, beside `dist/index.js`.\n *\n * It used to be `import workerUrl from './worker.ts?worker&url'`, which handed the\n * emit to Vite's worker plugin — and that plugin rewrote the one shape a consumer's\n * bundler can detect into `new URL(\"assets/worker-<hash>.js\", import.meta.url).href`\n * behind a `@vite-ignore`. Nothing static was left to detect, so a bundled app copied\n * the chunk as an opaque asset without ever processing it as a module, and the\n * `import('@huggingface/transformers')` inside it stayed a bare specifier no browser\n * can resolve: every model load failed. The worker is a second lib entry now, at a\n * stable `dist/worker.js`, and this package constructs it itself.\n */\nconst WORKER_FILE = './worker.js';\n\n/** The blob URL the worker was built from, if it needed one. Revoked with the worker. */\nlet _workerBlobUrl: string | null = null;\n\n/**\n * Build the worker — including when this package is served from another origin.\n *\n * `new Worker()` refuses a cross-origin script outright, and that is not an exotic\n * case: it is every deploy whose JavaScript lives on a CDN or an asset host while the\n * page lives somewhere else, with or without a bundler. Reproduced with the package on\n * one port and the page on another: `SecurityError: Script at '…/assets/worker-*.js'\n * cannot be accessed from origin '…'`.\n *\n * A blob inherits the ORIGIN OF THE DOCUMENT THAT CREATES IT, so a one-line blob whose\n * body imports the real worker by absolute URL is same-origin by construction, and the\n * import inside it is a normal cross-origin module fetch, which is allowed. It is the\n * shim ffmpeg.wasm and tesseract.js use for the same reason.\n *\n * Same-origin keeps the direct path: no blob, nothing to revoke, and a stack trace that\n * names the real file.\n */\nfunction _spawnWorker(): Worker {\n // Behind a constant, and that is not style either. Vite's asset transform rewrites\n // `new URL(<literal>, import.meta.url)` to a base of its own; behind a variable it\n // does not look, so this line keeps the REAL module URL — which is what the origin\n // comparison and the blob body below have to be built from. The build drops that\n // transform anyway (see `vite.config.ts`), so in the shipped bytes the two forms\n // resolve identically; under a dev server and under vitest they do not.\n const url = new URL(WORKER_FILE, import.meta.url);\n const sameOrigin = typeof location === 'undefined' || url.origin === location.origin;\n // A blob is the only way across an origin, so an environment that cannot mint one has\n // nothing to gain from trying: construct directly and let the platform say what it\n // thinks. jsdom is that environment — it has `Blob` and no `URL.createObjectURL` — and\n // every test in this package went through the blob path and threw before this line\n // existed.\n const canMintBlob = typeof Blob === 'function' && typeof URL.createObjectURL === 'function';\n // The literal below is not style, and it is written out rather than reusing\n // `WORKER_FILE` above. `new Worker(new URL('./worker.js', import.meta.url))` is the\n // exact shape Vite's worker detection and webpack's WorkerPlugin match on, and\n // matching it is what makes a CONSUMER's bundler process the worker as a module —\n // which is how `@huggingface/transformers` gets resolved inside it. Behind a variable\n // the file is copied as an opaque asset and its imports are never touched, so\n // hoisting this line to reuse `url` would fix a CDN page by breaking every bundled\n // app. That the SHIPPED bytes still carry it is asserted by\n // `src/__tests__/published-shape.test.ts`: the claim was true of this source and\n // false of the artifact for as long as the build owned the emit.\n if (sameOrigin || !canMintBlob) return new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });\n\n _workerBlobUrl = URL.createObjectURL(\n new Blob([`import ${JSON.stringify(url.href)};`], { type: 'text/javascript' }),\n );\n try {\n return new Worker(_workerBlobUrl, { type: 'module' });\n } catch (error) {\n // A page with `worker-src 'self'` (or `script-src` without `blob:`) blocks the\n // shim, and the direct URL was already refused for its origin — so there is\n // nothing left to try. Say which of the two walls was hit, because the browser's\n // own message does not distinguish them.\n URL.revokeObjectURL(_workerBlobUrl);\n _workerBlobUrl = null;\n throw new Error(\n `@aparte/provider-transformers is served from ${url.origin}, which is not this page's origin, `\n + 'so its worker has to be started through a blob: URL — and this page\\'s Content-Security-Policy '\n + 'refuses that. Allow `blob:` in `worker-src` (or `script-src`), or serve the package from your '\n + `own origin. Original error: ${String(error)}`,\n );\n }\n}\n\nfunction _releaseWorkerBlob(): void {\n if (_workerBlobUrl) {\n URL.revokeObjectURL(_workerBlobUrl);\n _workerBlobUrl = null;\n }\n}\n\n/**\n * Where the page says Transformers.js lives, if it says so at all.\n *\n * The worker cannot ask: an import map is the DOCUMENT's, and by spec it does not reach\n * a worker. The main thread can, and does it the platform's way — `import.meta.resolve`\n * consults that same map — so a page that already maps `@huggingface/transformers` (it\n * has to, to import this package by name at all) is telling us where its copy is. That\n * map is the CDN consumer's manifest: the version pin stays with the consumer, which is\n * the whole point of a peer dependency, and this package invents no second place to say\n * it.\n *\n * `undefined` under a bundler, where the specifier is resolved at build time and the\n * worker's own `import('@huggingface/transformers')` is the path that runs.\n */\nfunction _peerModuleUrl(): string | undefined {\n const resolve = (import.meta as unknown as { resolve?: (specifier: string) => string }).resolve;\n if (typeof resolve === 'function') {\n try {\n const href = resolve('@huggingface/transformers');\n if (href && /^https?:/i.test(href)) return href;\n } catch { /* not in the map — fall through */ }\n }\n // Older engines have no `import.meta.resolve`; read the map they do have.\n try {\n const el = document.querySelector('script[type=\"importmap\"]');\n const map = el?.textContent ? JSON.parse(el.textContent) as { imports?: Record<string, string> } : null;\n const href = map?.imports?.['@huggingface/transformers'];\n if (href) return new URL(href, location.href).href;\n } catch { /* no document, or a map that is not JSON */ }\n return undefined;\n}\n\nfunction _getWorker(): Worker {\n if (!_worker) {\n _worker = _spawnWorker();\n _worker.addEventListener('message', _handleWorkerMessage);\n _worker.addEventListener('error', _handleWorkerError);\n _worker.addEventListener('messageerror', _handleWorkerError);\n // First message, before any work: postMessage keeps order, so the worker has it\n // by the time a prepare or a generate needs the module.\n _worker.postMessage({ type: 'init', transformersUrl: _peerModuleUrl() });\n }\n return _worker;\n}\n\n/**\n * Worker crashed (uncaught error / WASM init failure / OOM). Reject every in-flight\n * prepare and close every open generate stream so the UI doesn't hang. Subsequent\n * calls rebuild the worker.\n */\nfunction _handleWorkerError(e: Event): void {\n const message = (e as ErrorEvent)?.message || 'Worker crashed unexpectedly';\n\n for (const p of _pendingPrepares.values()) {\n try { p.reject(new Error(message)); } catch { /* ignore */ }\n }\n _pendingPrepares.clear();\n\n for (const ctrl of _pendingGenerates.values()) {\n try { ctrl.enqueue({ type: 'error' as const, message }); ctrl.close(); }\n catch { /* ignore */ }\n }\n _pendingGenerates.clear();\n for (const c of _pendingCommands.values()) c.reject(new Error(message));\n _pendingCommands.clear();\n\n // Release every serialization slot so the generate chain doesn't deadlock.\n for (const resolve of _generateDoneResolvers.values()) {\n try { resolve(); } catch { /* ignore */ }\n }\n _generateDoneResolvers.clear();\n _generateChain = Promise.resolve();\n _queuedModelIds.clear();\n\n _loadedModelId = null;\n _preparingModelId = null;\n try { _worker?.terminate(); } catch { /* ignore */ }\n _worker = null;\n _releaseWorkerBlob();\n}\n\nfunction _handleWorkerMessage(event: MessageEvent): void {\n const msg = event.data;\n\n switch (msg.type) {\n case 'progress': {\n const pending = _pendingPrepares.get(msg.id);\n if (!pending) break;\n if (msg.status === 'ready') {\n pending.onProgress({ status: 'ready' });\n pending.resolve();\n _pendingPrepares.delete(msg.id);\n } else if (msg.status === 'loading') {\n pending.onProgress({ status: 'loading' });\n } else if (msg.status === 'cached') {\n pending.onProgress({ status: 'cached', file: msg.file, progress: msg.progress });\n } else {\n pending.onProgress({ status: 'downloading', file: msg.file, progress: msg.progress });\n }\n break;\n }\n case 'prepare-error': {\n const pending = _pendingPrepares.get(msg.id);\n if (!pending) break;\n pending.reject(new Error(msg.message));\n _pendingPrepares.delete(msg.id);\n if (_preparingModelId === pending.modelId) _preparingModelId = null;\n break;\n }\n case 'pipeline-ready': {\n _loadedModelId = msg.modelId;\n _preparingModelId = null;\n // Evict models over the cache limit, then refresh the known list.\n void _enforceMaxCachedModels(msg.modelId).then(() => _refreshKnownModels());\n break;\n }\n case 'gen-event': {\n // The runner speaks the stream vocabulary itself; nothing to translate.\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue(msg.event);\n break;\n }\n case 'warning': {\n // Already said once per text by the worker; the page just carries the voice.\n console.warn(`[transformers] ${msg.message}`);\n break;\n }\n case 'command-result': {\n _releaseGenerateSlot(msg.id);\n const pending = _pendingCommands.get(msg.id);\n if (!pending) break;\n _pendingCommands.delete(msg.id);\n if (msg.error !== undefined) pending.reject(new Error(msg.error));\n else pending.resolve(msg.result);\n break;\n }\n case 'gen-done': {\n _releaseGenerateSlot(msg.id);\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue({ type: 'done' as const, ...(msg.usage ? { usage: msg.usage } : {}) });\n ctrl.close();\n _pendingGenerates.delete(msg.id);\n break;\n }\n case 'gen-error': {\n _releaseGenerateSlot(msg.id);\n const ctrl = _pendingGenerates.get(msg.id);\n if (!ctrl) break;\n ctrl.enqueue({ type: 'error' as const, message: msg.message });\n ctrl.close();\n _pendingGenerates.delete(msg.id);\n break;\n }\n }\n}\n\n/**\n * Narrowed so the two members the docs tell you to CALL are not optional.\n *\n * `AparteAIProvider` declares `prepareModel` and `getModelStatus` optional (most\n * providers have nothing to download), and widening to it made both\n * possibly-undefined — so the documented `TransformersProvider.prepareModel(...)`\n * needed a `!` or a guard in every strict consumer. Same technique openai-compat\n * already used for its own always-present members.\n *\n * `chat` joined the list once `AparteAIProvider` became a union: it is optional on\n * the format-adapter arm, and this provider IS its `chat()` — running inference\n * locally is the whole package. Narrowing it here says so once, instead of every\n * caller writing `provider.chat!(...)`.\n */\nexport const TransformersProvider: AparteAIProvider\n & Required<Pick<AparteAIProvider, 'prepareModel' | 'getModelStatus' | 'chat'>> = {\n id: 'transformers',\n\n getMetadata() {\n return {\n id: 'transformers',\n name: 'Transformers.js',\n icon: `<svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12 2L2 7l10 5 10-5-10-5z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2 17l10 5 10-5\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2 12l10 5 10-5\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>`,\n color: '#f59e0b',\n description: 'Run LLMs directly in your browser via WebGPU or WASM — no API, no key',\n hasFreeModels: true,\n isLocal: true,\n helpUrl: 'https://huggingface.co/docs/transformers.js',\n };\n },\n\n getModels(): AparteAIModel[] {\n return _knownModels;\n },\n\n async fetchModels(): Promise<AparteAIModel[]> {\n await _refreshKnownModels();\n return _knownModels;\n },\n\n async chat(\n request: AparteChatRequest,\n _config?: string | Record<string, string>,\n ctx?: { providerId: string; signal?: AbortSignal },\n ): Promise<AparteChatResponse> {\n const requestId = uuid();\n const options = {\n maxTokens: request.maxTokens,\n temperature: request.temperature,\n seed: request.seed,\n };\n const signal = ctx?.signal;\n\n // ── Reserve a serialization slot ─────────────────────────────────────\n // Chain this generate behind the previous one; the worker has a single\n // pipeline, so generates MUST NOT overlap.\n _warnIfContended(request.modelId);\n _queuedModelIds.set(requestId, request.modelId);\n const prevGenerate = _generateChain;\n _generateChain = new Promise<void>((resolveSlot) => {\n _generateDoneResolvers.set(requestId, resolveSlot);\n });\n // ── Stop, from either side ───────────────────────────────────────────\n // The transport's `ctx.signal` (the user's Stop, which the provider contract\n // says a bridge MUST honour — this one read it nowhere) and the stream's own\n // `cancel()` say the same thing, and the worker hears it once. Before the\n // generate has been posted there is nothing to interrupt: the stream is\n // settled here, and the slot is released when its turn in the chain comes —\n // not earlier, or the next generate would start over the one still running.\n let posted = false;\n let stopped = false;\n const stop = (): void => {\n if (stopped) return;\n stopped = true;\n signal?.removeEventListener('abort', stop);\n const ctrl = _pendingGenerates.get(requestId);\n _pendingGenerates.delete(requestId);\n if (posted) {\n _getWorker().postMessage({ type: 'cancel', id: requestId });\n // The reply ends HERE, not when the worker gets round to answering:\n // a token already in flight was otherwise enqueued into a stream the\n // user had stopped. `done` and not an error — a stop is not a failure,\n // and it is what settles the non-streaming path too. The worker's own\n // `gen-done` still releases the queue slot: that happens before it\n // looks the controller up.\n try { ctrl?.enqueue({ type: 'done' as const }); ctrl?.close(); } catch { /* already closed */ }\n return;\n }\n if (!ctrl) return;\n try { ctrl.enqueue({ type: 'error' as const, message: 'Generation cancelled before it started' }); ctrl.close(); }\n catch { /* already closed */ }\n };\n const postGenerate = (): void => {\n if (stopped) { _releaseGenerateSlot(requestId); return; }\n posted = true;\n _getWorker().postMessage({\n type: 'generate',\n id: requestId,\n modelId: request.modelId,\n // The conversation as it is, parts included: which parts a model can take\n // is the runner's knowledge, not this thread's.\n messages: request.messages,\n options,\n ..._selection(request.modelId),\n });\n };\n\n let response: AparteChatResponse | Promise<string>;\n if (request.stream === false) {\n response = new Promise<string>((resolve, reject) => {\n let result = '';\n const fakeCtrl = {\n enqueue: (chunk: { type: string; delta?: string; message?: string }) => {\n if (chunk.type === 'text') result += chunk.delta ?? '';\n else if (chunk.type === 'done') resolve(result);\n else if (chunk.type === 'error') reject(new Error(chunk.message));\n },\n close: () => { /* no-op */ },\n } as unknown as ReadableStreamDefaultController;\n _pendingGenerates.set(requestId, fakeCtrl);\n void prevGenerate.then(postGenerate);\n });\n } else {\n response = new ReadableStream({\n async start(controller) {\n _pendingGenerates.set(requestId, controller);\n await prevGenerate;\n postGenerate();\n },\n cancel() {\n // The reader is gone, so nothing may be enqueued for it again — and the\n // model actually STOPS (not just the read): the worker interrupts this\n // generate, and the slot is still released by the resulting\n // gen-done/gen-error, so a queued generate cannot start before that.\n _pendingGenerates.delete(requestId);\n stop();\n },\n });\n }\n\n // `start` has run by now, so the controller is registered and a stop settles it.\n if (signal?.aborted) stop();\n else signal?.addEventListener('abort', stop, { once: true });\n return response;\n },\n\n async getModelStatus(modelId: string): Promise<ModelStatus> {\n if (_loadedModelId === modelId) return 'ready';\n if (_preparingModelId === modelId) return 'cached';\n if ('caches' in globalThis) {\n try {\n const encodedId = encodeURIComponent(modelId);\n const names = await caches.keys();\n for (const name of names) {\n const cache = await caches.open(name);\n const keys = await cache.keys();\n if (keys.some(r => r.url.includes(encodedId) || r.url.includes(modelId + '/'))) {\n return 'cached';\n }\n }\n } catch {\n // Cache API unavailable\n }\n }\n return 'not-downloaded';\n },\n\n async prepareModel(modelId: string, onProgress: (p: ModelLoadProgress) => void): Promise<void> {\n if (_loadedModelId === modelId) {\n onProgress({ status: 'ready' });\n return;\n }\n\n const requestId = uuid();\n _preparingModelId = modelId;\n\n return new Promise<void>((resolve, reject) => {\n _pendingPrepares.set(requestId, { modelId, onProgress, resolve, reject });\n _getWorker().postMessage({ type: 'prepare', id: requestId, modelId, ..._selection(modelId) });\n });\n },\n\n async deleteModel(modelId: string): Promise<void> {\n await deleteCachedModel(modelId);\n },\n};\n\nexport default TransformersProvider;\nexport type { AparteAIProvider, AparteAIModel, ModelStatus, ModelLoadProgress } from '@aparte/core';\nexport type {\n TransformersRunner,\n RunnerContext,\n RunnerGenerateInput,\n RunnerProgress,\n RunnerModule,\n CreateRunner,\n BuiltInRunner,\n TransformersModule,\n} from './runners/types.js';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cache utilities (settings panels, etc.)\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Returns the modelId currently loaded in the worker's pipeline, or null. */\nexport function getLoadedModelId(): string | null {\n return _loadedModelId;\n}\n\n/**\n * Send a runner something that is not a generation — swap an adapter, warm a cache, ask\n * a capability — and get its answer. The name and payload are the runner's vocabulary\n * (the built-in runners answer none). Queued behind the generates in flight: the worker\n * holds one runner, and a command on it mid-stream would race the stream.\n */\nexport function runnerCommand(modelId: string, name: string, payload: unknown): Promise<unknown> {\n const requestId = uuid();\n _queuedModelIds.set(requestId, modelId);\n const previous = _generateChain;\n _generateChain = new Promise<void>((resolveSlot) => {\n _generateDoneResolvers.set(requestId, resolveSlot);\n });\n return new Promise<unknown>((resolve, reject) => {\n _pendingCommands.set(requestId, { resolve, reject });\n void previous.then(() => {\n _getWorker().postMessage({ type: 'command', id: requestId, modelId, name, payload, ..._selection(modelId) });\n });\n });\n}\n\n/** Terminate the shared worker and reset in-memory state. Safe to call any time. */\nexport function terminateWorker(): void {\n _worker?.terminate();\n _worker = null;\n _releaseWorkerBlob();\n _loadedModelId = null;\n _preparingModelId = null;\n for (const [, p] of _pendingPrepares) {\n p.reject(new Error('Worker terminated'));\n }\n _pendingPrepares.clear();\n for (const [, ctrl] of _pendingGenerates) {\n try { ctrl.enqueue({ type: 'error' as const, message: 'Worker terminated' }); ctrl.close(); } catch { /* already closed */ }\n }\n _pendingGenerates.clear();\n for (const c of _pendingCommands.values()) c.reject(new Error('Worker terminated'));\n _pendingCommands.clear();\n\n // Release every serialization slot and reset the chain — the same three lines\n // the worker-error handler above already carried, with the same reason. Without\n // them, terminating mid-generate left `_generateChain` pending on a resolver\n // that had just been dropped, so the NEXT chat() awaited a promise that could\n // never settle: no error, no rejection, the stream simply never started again\n // for the life of the page.\n for (const resolve of _generateDoneResolvers.values()) {\n try { resolve(); } catch { /* ignore */ }\n }\n _generateDoneResolvers.clear();\n _generateChain = Promise.resolve();\n _queuedModelIds.clear();\n // A terminated worker is a fresh situation; let the contention warning speak again.\n _warnedModelContention = false;\n}\n\nexport interface CachedModelEntry {\n modelId: string;\n name: string;\n /** Total size in bytes of all cached files for this model. -1 if unknown. */\n sizeBytes: number;\n /** True if the model is currently loaded in the worker. */\n loaded: boolean;\n}\n\n/**\n * Scan the Cache API to find which Transformers.js models have been downloaded,\n * by matching cache entry URLs against the Hugging Face resolve path.\n */\nexport async function listCachedModels(): Promise<CachedModelEntry[]> {\n if (!('caches' in globalThis)) return [];\n\n const found = new Map<string, { name: string; sizeBytes: number }>();\n\n // e.g. https://huggingface.co/onnx-community/Qwen2.5-0.5B/resolve/main/config.json\n // → onnx-community/Qwen2.5-0.5B\n function extractModelId(url: string): string | null {\n const m = url.match(/huggingface\\.co\\/([^/]+\\/[^/]+)\\/resolve\\//);\n return m ? decodeURIComponent(m[1]!) : null;\n }\n\n function modelName(modelId: string): string {\n const config = _registeredModels.get(modelId);\n if (config) return config.name;\n return (modelId.split('/').pop() ?? modelId).replace(/-/g, ' ');\n }\n\n try {\n const cacheNames = await caches.keys();\n await Promise.all(cacheNames.map(async (cacheName) => {\n try {\n const cache = await caches.open(cacheName);\n const requests = await cache.keys();\n for (const req of requests) {\n const modelId = extractModelId(req.url);\n if (!modelId) continue;\n if (!found.has(modelId)) {\n found.set(modelId, { name: modelName(modelId), sizeBytes: 0 });\n }\n const response = await cache.match(req);\n if (!response) continue;\n const contentLength = response.headers.get('content-length');\n if (contentLength) {\n found.get(modelId)!.sizeBytes += parseInt(contentLength, 10);\n } else {\n try {\n const blob = await response.clone().blob();\n found.get(modelId)!.sizeBytes += blob.size;\n } catch { /* skip */ }\n }\n }\n } catch { /* skip inaccessible cache */ }\n }));\n } catch {\n return [];\n }\n\n return Array.from(found.entries()).map(([modelId, { name, sizeBytes }]) => ({\n modelId,\n name,\n sizeBytes,\n loaded: _loadedModelId === modelId,\n }));\n}\n\n/**\n * Delete all cached files for a modelId from the Cache API, terminating the worker\n * first if that model is currently loaded.\n */\nexport async function deleteCachedModel(modelId: string): Promise<void> {\n if (_loadedModelId === modelId || _preparingModelId === modelId) {\n terminateWorker();\n }\n if (!('caches' in globalThis)) return;\n try {\n const cacheNames = await caches.keys();\n await Promise.all(cacheNames.map(async (cacheName) => {\n try {\n const cache = await caches.open(cacheName);\n const requests = await cache.keys();\n const encoded = encodeURIComponent(modelId);\n await Promise.all(\n requests\n .filter(r => r.url.includes(modelId) || r.url.includes(encoded))\n .map(r => cache.delete(r)),\n );\n } catch { /* skip */ }\n }));\n } catch { /* Cache API unavailable */ }\n}\n"],"names":[],"mappings":"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"}
|
|
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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-text-to-text.d.ts","sourceRoot":"","sources":["../../src/runners/image-text-to-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,YAAY,CAAC;AAGpE,KAAK,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjE,KAAK,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE9E;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG;IAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA6BtI;
|
|
1
|
+
{"version":3,"file":"image-text-to-text.d.ts","sourceRoot":"","sources":["../../src/runners/image-text-to-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,YAAY,CAAC;AAGpE,KAAK,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjE,KAAK,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE9E;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG;IAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA6BtI;AAeD,eAAO,MAAM,YAAY,EAAE,YAuC1B,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as loadOptions, i as interruptOn, t as textStreamer, g as generationOptions, T as TOOL_TURNS_DROPPED, U as UNSUPPORTED_PARTS_DROPPED } from "./shared.js";
|
|
1
|
+
import { l as loadOptions, c as contextLength, i as interruptOn, s as stoppingCriteria, t as textStreamer, g as generationOptions, T as TOOL_TURNS_DROPPED, U as UNSUPPORTED_PARTS_DROPPED } from "./shared.js";
|
|
2
2
|
function toChatTemplate(messages, warn) {
|
|
3
3
|
const chat = [];
|
|
4
4
|
const images = [];
|
|
@@ -35,6 +35,7 @@ const createRunner = async (ctx) => {
|
|
|
35
35
|
tf.AutoProcessor.from_pretrained(ctx.modelId),
|
|
36
36
|
tf.AutoModelForImageTextToText.from_pretrained(ctx.modelId, loadOptions(ctx))
|
|
37
37
|
]);
|
|
38
|
+
const contextSize = contextLength(model.config);
|
|
38
39
|
return {
|
|
39
40
|
async generate({ messages, options, emit, signal }) {
|
|
40
41
|
const { stopping, release } = interruptOn(signal, ctx.transformers);
|
|
@@ -46,7 +47,7 @@ const createRunner = async (ctx) => {
|
|
|
46
47
|
...inputs,
|
|
47
48
|
...generationOptions(options),
|
|
48
49
|
streamer: textStreamer(ctx.transformers, processor.tokenizer, emit),
|
|
49
|
-
stopping_criteria: stopping
|
|
50
|
+
stopping_criteria: stoppingCriteria(stopping, ctx.transformers, contextSize)
|
|
50
51
|
});
|
|
51
52
|
} finally {
|
|
52
53
|
release();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-text-to-text.js","sources":["../../src/runners/image-text-to-text.ts"],"sourcesContent":["/**\n * The built-in vision runner — a model that reads images and text and writes text\n * (SmolVLM, Qwen2-VL, LFM2-VL, Gemma 3, LLaVA…: everything `AutoModelForImageTextToText`\n * resolves).\n *\n * Transformers.js 4.x has no `image-text-to-text` PIPELINE, so this runner goes through\n * the model classes themselves, the way the SmolVLM examples do: `AutoProcessor` renders\n * the chat template with `{ type: 'image' }` placeholders, the images are decoded beside\n * the prompt in the same order, the processor turns both into tensors, and `generate()`\n * streams through a `TextStreamer`. Tool turns are dropped with the shared warning.\n */\n\nimport type { AparteChatMessage } from '@aparte/core';\nimport type { CreateRunner, RunnerGenerateInput } from './types.js';\nimport { TOOL_TURNS_DROPPED, UNSUPPORTED_PARTS_DROPPED, generationOptions, interruptOn, loadOptions, textStreamer } from './shared.js';\n\ntype HFPart = { type: 'image' } | { type: 'text'; text: string };\ntype HFMessage = { role: 'user' | 'assistant' | 'system'; content: HFPart[] };\n\n/**\n * The conversation in the HF chat shape the processor's template expects — every turn's\n * content as parts, an `{ type: 'image' }` placeholder where a picture goes — plus the\n * pictures themselves, in order of appearance, for the processor to pair with them.\n */\nexport function toChatTemplate(messages: AparteChatMessage[], warn: (message: string) => void): { chat: HFMessage[]; images: string[] } {\n const chat: HFMessage[] = [];\n const images: string[] = [];\n let droppedToolTurns = 0;\n let droppedParts = 0;\n for (const m of messages) {\n if (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'system') { droppedToolTurns++; continue; }\n // An assistant's calls ride on an `assistant` message, which passes the role test\n // above and, when the model said nothing before them, carries no part either — so\n // without this the whole turn leaves the prompt with nothing said.\n if (m.toolCalls?.length) droppedToolTurns++;\n const parts: HFPart[] = [];\n if (typeof m.content === 'string') {\n if (m.content) parts.push({ type: 'text', text: m.content });\n } else {\n for (const p of m.content) {\n if (p.type === 'text') { if (p.text) parts.push({ type: 'text', text: p.text }); }\n else if (p.type === 'image') { images.push(p.image); parts.push({ type: 'image' }); }\n // Not an arm the union can produce, and the mirror of the role-axis guard the\n // wire mappers carry: an app built against an older aparte can still hand us a\n // `file` part, and pushing its absent `image` gave `load_image(undefined)`.\n else droppedParts++;\n }\n }\n if (parts.length > 0) chat.push({ role: m.role, content: parts });\n }\n if (droppedToolTurns > 0) warn(TOOL_TURNS_DROPPED);\n if (droppedParts > 0) warn(UNSUPPORTED_PARTS_DROPPED);\n return { chat, images };\n}\n\n/** The processor and model, as this runner calls them (Transformers.js types them through `Callable`). */\ninterface VisionProcessor {\n (text: string, images: unknown[]): Promise<Record<string, unknown>>;\n /** The text half alone — callable, for a turn that carries no picture. */\n tokenizer: (text: string) => Record<string, unknown> | Promise<Record<string, unknown>>;\n apply_chat_template(messages: HFMessage[], options: Record<string, unknown>): unknown;\n}\ninterface VisionModel {\n generate(args: Record<string, unknown>): Promise<unknown>;\n dispose(): Promise<unknown>;\n}\n\nexport const createRunner: CreateRunner = async (ctx) => {\n const tf = ctx.transformers as unknown as {\n AutoProcessor: { from_pretrained(model: string, options?: unknown): Promise<VisionProcessor> };\n AutoModelForImageTextToText: { from_pretrained(model: string, options?: unknown): Promise<VisionModel> };\n load_image(source: string): Promise<unknown>;\n };\n // The processor carries no weights: dtype and device are the model's alone.\n const [processor, model] = await Promise.all([\n tf.AutoProcessor.from_pretrained(ctx.modelId),\n tf.AutoModelForImageTextToText.from_pretrained(ctx.modelId, loadOptions(ctx)),\n ]);\n\n return {\n async generate({ messages, options, emit, signal }: RunnerGenerateInput): Promise<void> {\n const { stopping, release } = interruptOn(signal, ctx.transformers);\n try {\n const { chat, images } = toChatTemplate(messages, ctx.warn);\n const prompt = processor.apply_chat_template(chat, { add_generation_prompt: true, tokenize: false }) as string;\n // The processor pairs the prompt with pictures and wants at least one (Idefics3's\n // reads `images.rows` — \"hello\" as a first message crashed it on a real SmolVLM).\n // A turn with no image is text: the tokenizer alone takes it.\n const inputs = images.length > 0\n ? await processor(prompt, await Promise.all(images.map((src) => tf.load_image(src))))\n : await processor.tokenizer(prompt);\n await model.generate({\n ...inputs,\n ...generationOptions(options),\n streamer: textStreamer(ctx.transformers, processor.tokenizer, emit),\n stopping_criteria: stopping,\n });\n } finally {\n release();\n }\n },\n async dispose() {\n await model.dispose();\n },\n };\n};\n"],"names":[],"mappings":";AAwBO,SAAS,eAAe,UAA+B,MAA0E;AACpI,QAAM,OAAoB,CAAA;AAC1B,QAAM,SAAmB,CAAA;AACzB,MAAI,mBAAmB;AACvB,MAAI,eAAe;AACnB,aAAW,KAAK,UAAU;AACtB,QAAI,EAAE,SAAS,UAAU,EAAE,SAAS,eAAe,EAAE,SAAS,UAAU;AAAE;AAAoB;AAAA,IAAU;AAIxG,QAAI,EAAE,WAAW,OAAQ;AACzB,UAAM,QAAkB,CAAA;AACxB,QAAI,OAAO,EAAE,YAAY,UAAU;AAC/B,UAAI,EAAE,QAAS,OAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,EAAE,SAAS;AAAA,IAC/D,OAAO;AACH,iBAAW,KAAK,EAAE,SAAS;AACvB,YAAI,EAAE,SAAS,QAAQ;AAAE,cAAI,EAAE,KAAM,OAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,EAAE,MAAM;AAAA,QAAG,WACxE,EAAE,SAAS,SAAS;AAAE,iBAAO,KAAK,EAAE,KAAK;AAAG,gBAAM,KAAK,EAAE,MAAM,QAAA,CAAS;AAAA,QAAG,MAI/E;AAAA,MACT;AAAA,IACJ;AACA,QAAI,MAAM,SAAS,EAAG,MAAK,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,MAAA,CAAO;AAAA,EACpE;AACA,MAAI,mBAAmB,EAAG,MAAK,kBAAkB;AACjD,MAAI,eAAe,EAAG,MAAK,yBAAyB;AACpD,SAAO,EAAE,MAAM,OAAA;AACnB;
|
|
1
|
+
{"version":3,"file":"image-text-to-text.js","sources":["../../src/runners/image-text-to-text.ts"],"sourcesContent":["/**\n * The built-in vision runner — a model that reads images and text and writes text\n * (SmolVLM, Qwen2-VL, LFM2-VL, Gemma 3, LLaVA…: everything `AutoModelForImageTextToText`\n * resolves).\n *\n * Transformers.js 4.x has no `image-text-to-text` PIPELINE, so this runner goes through\n * the model classes themselves, the way the SmolVLM examples do: `AutoProcessor` renders\n * the chat template with `{ type: 'image' }` placeholders, the images are decoded beside\n * the prompt in the same order, the processor turns both into tensors, and `generate()`\n * streams through a `TextStreamer`. Tool turns are dropped with the shared warning.\n */\n\nimport type { AparteChatMessage } from '@aparte/core';\nimport type { CreateRunner, RunnerGenerateInput } from './types.js';\nimport { TOOL_TURNS_DROPPED, UNSUPPORTED_PARTS_DROPPED, contextLength, generationOptions, interruptOn, loadOptions, stoppingCriteria, textStreamer } from './shared.js';\n\ntype HFPart = { type: 'image' } | { type: 'text'; text: string };\ntype HFMessage = { role: 'user' | 'assistant' | 'system'; content: HFPart[] };\n\n/**\n * The conversation in the HF chat shape the processor's template expects — every turn's\n * content as parts, an `{ type: 'image' }` placeholder where a picture goes — plus the\n * pictures themselves, in order of appearance, for the processor to pair with them.\n */\nexport function toChatTemplate(messages: AparteChatMessage[], warn: (message: string) => void): { chat: HFMessage[]; images: string[] } {\n const chat: HFMessage[] = [];\n const images: string[] = [];\n let droppedToolTurns = 0;\n let droppedParts = 0;\n for (const m of messages) {\n if (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'system') { droppedToolTurns++; continue; }\n // An assistant's calls ride on an `assistant` message, which passes the role test\n // above and, when the model said nothing before them, carries no part either — so\n // without this the whole turn leaves the prompt with nothing said.\n if (m.toolCalls?.length) droppedToolTurns++;\n const parts: HFPart[] = [];\n if (typeof m.content === 'string') {\n if (m.content) parts.push({ type: 'text', text: m.content });\n } else {\n for (const p of m.content) {\n if (p.type === 'text') { if (p.text) parts.push({ type: 'text', text: p.text }); }\n else if (p.type === 'image') { images.push(p.image); parts.push({ type: 'image' }); }\n // Not an arm the union can produce, and the mirror of the role-axis guard the\n // wire mappers carry: an app built against an older aparte can still hand us a\n // `file` part, and pushing its absent `image` gave `load_image(undefined)`.\n else droppedParts++;\n }\n }\n if (parts.length > 0) chat.push({ role: m.role, content: parts });\n }\n if (droppedToolTurns > 0) warn(TOOL_TURNS_DROPPED);\n if (droppedParts > 0) warn(UNSUPPORTED_PARTS_DROPPED);\n return { chat, images };\n}\n\n/** The processor and model, as this runner calls them (Transformers.js types them through `Callable`). */\ninterface VisionProcessor {\n (text: string, images: unknown[]): Promise<Record<string, unknown>>;\n /** The text half alone — callable, for a turn that carries no picture. */\n tokenizer: (text: string) => Record<string, unknown> | Promise<Record<string, unknown>>;\n apply_chat_template(messages: HFMessage[], options: Record<string, unknown>): unknown;\n}\ninterface VisionModel {\n generate(args: Record<string, unknown>): Promise<unknown>;\n dispose(): Promise<unknown>;\n config?: unknown;\n}\n\nexport const createRunner: CreateRunner = async (ctx) => {\n const tf = ctx.transformers as unknown as {\n AutoProcessor: { from_pretrained(model: string, options?: unknown): Promise<VisionProcessor> };\n AutoModelForImageTextToText: { from_pretrained(model: string, options?: unknown): Promise<VisionModel> };\n load_image(source: string): Promise<unknown>;\n };\n // The processor carries no weights: dtype and device are the model's alone.\n const [processor, model] = await Promise.all([\n tf.AutoProcessor.from_pretrained(ctx.modelId),\n tf.AutoModelForImageTextToText.from_pretrained(ctx.modelId, loadOptions(ctx)),\n ]);\n const contextSize = contextLength(model.config);\n\n return {\n async generate({ messages, options, emit, signal }: RunnerGenerateInput): Promise<void> {\n const { stopping, release } = interruptOn(signal, ctx.transformers);\n try {\n const { chat, images } = toChatTemplate(messages, ctx.warn);\n const prompt = processor.apply_chat_template(chat, { add_generation_prompt: true, tokenize: false }) as string;\n // The processor pairs the prompt with pictures and wants at least one (Idefics3's\n // reads `images.rows` — \"hello\" as a first message crashed it on a real SmolVLM).\n // A turn with no image is text: the tokenizer alone takes it.\n const inputs = images.length > 0\n ? await processor(prompt, await Promise.all(images.map((src) => tf.load_image(src))))\n : await processor.tokenizer(prompt);\n await model.generate({\n ...inputs,\n ...generationOptions(options),\n streamer: textStreamer(ctx.transformers, processor.tokenizer, emit),\n stopping_criteria: stoppingCriteria(stopping, ctx.transformers, contextSize),\n });\n } finally {\n release();\n }\n },\n async dispose() {\n await model.dispose();\n },\n };\n};\n"],"names":[],"mappings":";AAwBO,SAAS,eAAe,UAA+B,MAA0E;AACpI,QAAM,OAAoB,CAAA;AAC1B,QAAM,SAAmB,CAAA;AACzB,MAAI,mBAAmB;AACvB,MAAI,eAAe;AACnB,aAAW,KAAK,UAAU;AACtB,QAAI,EAAE,SAAS,UAAU,EAAE,SAAS,eAAe,EAAE,SAAS,UAAU;AAAE;AAAoB;AAAA,IAAU;AAIxG,QAAI,EAAE,WAAW,OAAQ;AACzB,UAAM,QAAkB,CAAA;AACxB,QAAI,OAAO,EAAE,YAAY,UAAU;AAC/B,UAAI,EAAE,QAAS,OAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,EAAE,SAAS;AAAA,IAC/D,OAAO;AACH,iBAAW,KAAK,EAAE,SAAS;AACvB,YAAI,EAAE,SAAS,QAAQ;AAAE,cAAI,EAAE,KAAM,OAAM,KAAK,EAAE,MAAM,QAAQ,MAAM,EAAE,MAAM;AAAA,QAAG,WACxE,EAAE,SAAS,SAAS;AAAE,iBAAO,KAAK,EAAE,KAAK;AAAG,gBAAM,KAAK,EAAE,MAAM,QAAA,CAAS;AAAA,QAAG,MAI/E;AAAA,MACT;AAAA,IACJ;AACA,QAAI,MAAM,SAAS,EAAG,MAAK,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,MAAA,CAAO;AAAA,EACpE;AACA,MAAI,mBAAmB,EAAG,MAAK,kBAAkB;AACjD,MAAI,eAAe,EAAG,MAAK,yBAAyB;AACpD,SAAO,EAAE,MAAM,OAAA;AACnB;AAeO,MAAM,eAA6B,OAAO,QAAQ;AACrD,QAAM,KAAK,IAAI;AAMf,QAAM,CAAC,WAAW,KAAK,IAAI,MAAM,QAAQ,IAAI;AAAA,IACzC,GAAG,cAAc,gBAAgB,IAAI,OAAO;AAAA,IAC5C,GAAG,4BAA4B,gBAAgB,IAAI,SAAS,YAAY,GAAG,CAAC;AAAA,EAAA,CAC/E;AACD,QAAM,cAAc,cAAc,MAAM,MAAM;AAE9C,SAAO;AAAA,IACH,MAAM,SAAS,EAAE,UAAU,SAAS,MAAM,UAA8C;AACpF,YAAM,EAAE,UAAU,QAAA,IAAY,YAAY,QAAQ,IAAI,YAAY;AAClE,UAAI;AACA,cAAM,EAAE,MAAM,OAAA,IAAW,eAAe,UAAU,IAAI,IAAI;AAC1D,cAAM,SAAS,UAAU,oBAAoB,MAAM,EAAE,uBAAuB,MAAM,UAAU,OAAO;AAInG,cAAM,SAAS,OAAO,SAAS,IACzB,MAAM,UAAU,QAAQ,MAAM,QAAQ,IAAI,OAAO,IAAI,CAAC,QAAQ,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,IAClF,MAAM,UAAU,UAAU,MAAM;AACtC,cAAM,MAAM,SAAS;AAAA,UACjB,GAAG;AAAA,UACH,GAAG,kBAAkB,OAAO;AAAA,UAC5B,UAAU,aAAa,IAAI,cAAc,UAAU,WAAW,IAAI;AAAA,UAClE,mBAAmB,iBAAiB,UAAU,IAAI,cAAc,WAAW;AAAA,QAAA,CAC9E;AAAA,MACL,UAAA;AACI,gBAAA;AAAA,MACJ;AAAA,IACJ;AAAA,IACA,MAAM,UAAU;AACZ,YAAM,MAAM,QAAA;AAAA,IAChB;AAAA,EAAA;AAER;"}
|
package/dist/runners/shared.d.ts
CHANGED
|
@@ -31,9 +31,33 @@ export declare function interruptOn(signal: AbortSignal, transformers: Transform
|
|
|
31
31
|
};
|
|
32
32
|
/** A `TextStreamer` that emits each decoded token as a `text` event, prompt skipped. */
|
|
33
33
|
export declare function textStreamer(transformers: TransformersModule, tokenizer: unknown, emit: (event: AparteStreamEvent) => void): unknown;
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The reply's ceiling when no `maxTokens` is set: long enough that an answer is not cut
|
|
36
|
+
* (some 6,000 words), short enough that a small model stuck repeating itself stops. Decoding
|
|
37
|
+
* is greedy by default, and in the browser the attention cache grows with every token — a
|
|
38
|
+
* loop left to run to a 131,072-token window ends in a GPU out-of-memory, not a clean stop.
|
|
39
|
+
*/
|
|
40
|
+
export declare const DEFAULT_MAX_NEW_TOKENS = 8192;
|
|
41
|
+
/**
|
|
42
|
+
* How many tokens the model's window holds, prompt and reply together, read from its
|
|
43
|
+
* config — a vision model keeps it on the text half. `undefined` when the config says nothing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function contextLength(config: unknown): number | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Sampling options in Transformers.js' vocabulary, from the request's. The reply's ceiling is
|
|
48
|
+
* the caller's `maxTokens`, else {@link DEFAULT_MAX_NEW_TOKENS} — always set: left out, the
|
|
49
|
+
* text-generation pipeline would apply its own default of 256.
|
|
50
|
+
*/
|
|
35
51
|
export declare function generationOptions(options: {
|
|
36
52
|
maxTokens?: number;
|
|
37
53
|
temperature?: number;
|
|
38
54
|
}): Record<string, unknown>;
|
|
55
|
+
/**
|
|
56
|
+
* The stopping criteria a generate runs with: the Stop signal's, plus — when the model's
|
|
57
|
+
* window is known — one that ends the reply when prompt and reply fill that window. A ceiling
|
|
58
|
+
* larger than what the window has left would otherwise run the model past its context, where
|
|
59
|
+
* it degrades; this bounds every ceiling, the default and the caller's, without having to
|
|
60
|
+
* count the prompt's tokens first.
|
|
61
|
+
*/
|
|
62
|
+
export declare function stoppingCriteria(stopping: unknown, transformers: TransformersModule, contextSize: number | undefined): unknown;
|
|
39
63
|
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/runners/shared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEpE,eAAO,MAAM,kBAAkB,QAI6C,CAAC;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAIJ,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUvE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,OAAO,IAAI,IAAI,CAAA;CAAE,CAMzH;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,YAAY,EAAE,kBAAkB,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAOpI;AAED,
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/runners/shared.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEpE,eAAO,MAAM,kBAAkB,QAI6C,CAAC;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAIJ,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUvE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,kBAAkB,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,OAAO,IAAI,IAAI,CAAA;CAAE,CAMzH;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,YAAY,EAAE,kBAAkB,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAOpI;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAOjE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOhH;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAI9H"}
|
package/dist/runners/shared.js
CHANGED
|
@@ -32,20 +32,36 @@ function textStreamer(transformers, tokenizer, emit) {
|
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
const DEFAULT_MAX_NEW_TOKENS = 8192;
|
|
36
|
+
function contextLength(config) {
|
|
37
|
+
const c = config;
|
|
38
|
+
const text = c?.["text_config"];
|
|
39
|
+
for (const value of [c?.["max_position_embeddings"], text?.["max_position_embeddings"], c?.["n_positions"]]) {
|
|
40
|
+
if (typeof value === "number" && value > 0) return value;
|
|
41
|
+
}
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
35
44
|
function generationOptions(options) {
|
|
36
45
|
const temperature = options.temperature ?? 0;
|
|
37
46
|
return {
|
|
38
|
-
max_new_tokens: options.maxTokens ??
|
|
47
|
+
max_new_tokens: options.maxTokens ?? DEFAULT_MAX_NEW_TOKENS,
|
|
39
48
|
do_sample: temperature > 0,
|
|
40
49
|
temperature: temperature > 0 ? temperature : void 0
|
|
41
50
|
};
|
|
42
51
|
}
|
|
52
|
+
function stoppingCriteria(stopping, transformers, contextSize) {
|
|
53
|
+
if (contextSize === void 0) return stopping;
|
|
54
|
+
const MaxLengthCriteria = transformers.MaxLengthCriteria;
|
|
55
|
+
return [stopping, new MaxLengthCriteria(contextSize)];
|
|
56
|
+
}
|
|
43
57
|
export {
|
|
44
58
|
TOOL_TURNS_DROPPED as T,
|
|
45
59
|
UNSUPPORTED_PARTS_DROPPED as U,
|
|
60
|
+
contextLength as c,
|
|
46
61
|
generationOptions as g,
|
|
47
62
|
interruptOn as i,
|
|
48
63
|
loadOptions as l,
|
|
64
|
+
stoppingCriteria as s,
|
|
49
65
|
textStreamer as t
|
|
50
66
|
};
|
|
51
67
|
//# sourceMappingURL=shared.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared.js","sources":["../../src/runners/shared.ts"],"sourcesContent":["/**\n * What the two built-in runners share — kept in one place so the two cannot drift on\n * how progress is reported, how a stop reaches the model, or what a dropped tool turn\n * says. Types only from core (see `text-generation.ts` for why).\n */\n\nimport type { AparteStreamEvent } from '@aparte/core';\nimport type { RunnerContext, TransformersModule } from './types.js';\n\nexport const TOOL_TURNS_DROPPED =\n 'Dropped the tool call(s) and their results from the prompt: this runner does not support '\n + 'tool calling, so the model sees neither the call the assistant made nor the result that '\n + 'answered it. What the assistant SAID before calling stays in the prompt. Use an '\n + 'OpenAI-compatible endpoint for tools, or a runner that renders them.';\n\n/**\n * A content part neither built-in runner can carry. The union is text and image, so this\n * is unreachable from typed code — and reachable all the same from an app built against an\n * older aparte, which declared a third `file` part nothing ever filled. The wire mappers\n * guard the same case on the ROLE axis (`openai-compat`'s removed `tool_call`/`tool_result`);\n * the part axis had no guard, so the part was pushed into the image list as `undefined` and\n * `load_image` threw at generate time instead of the part being counted and named.\n */\nexport const UNSUPPORTED_PARTS_DROPPED =\n 'Dropped content part(s) this runner cannot carry: it takes text and image parts only, so '\n + 'anything else leaves the prompt with nothing in its place. A `file` part is the case to '\n + 'expect — it was removed from `AparteContentPart`; inline what you want the model to read '\n + 'as text, or send an image.';\n\n/**\n * The options a `from_pretrained` / `pipeline()` call takes from the context: download\n * progress forwarded to the page (percentages, rounded), dtype and device when set.\n */\nexport function loadOptions(ctx: RunnerContext): Record<string, unknown> {\n const opts: Record<string, unknown> = {\n progress_callback: (p: { status?: string; file?: string; progress?: number }) => {\n if (p.status === 'progress') ctx.progress({ status: 'downloading', file: p.file, progress: Math.round(p.progress ?? 0) });\n else if (p.status === 'done') ctx.progress({ status: 'loading', file: p.file });\n },\n };\n if (ctx.dtype) opts['dtype'] = ctx.dtype;\n if (ctx.device && ctx.device !== 'auto') opts['device'] = ctx.device;\n return opts;\n}\n\n/**\n * A stopping criteria the signal interrupts — so a Stop actually STOPS the model, not just\n * the read; otherwise generation runs to `max_new_tokens` off-thread, spending exactly the\n * CPU/GPU/battery this provider exists to save. Call `release()` in a `finally`.\n */\nexport function interruptOn(signal: AbortSignal, transformers: TransformersModule): { stopping: unknown; release(): void } {\n const stopping = new transformers.InterruptableStoppingCriteria();\n const onAbort = (): void => { stopping.interrupt(); };\n if (signal.aborted) onAbort();\n else signal.addEventListener('abort', onAbort, { once: true });\n return { stopping, release: () => { signal.removeEventListener('abort', onAbort); } };\n}\n\n/** A `TextStreamer` that emits each decoded token as a `text` event, prompt skipped. */\nexport function textStreamer(transformers: TransformersModule, tokenizer: unknown, emit: (event: AparteStreamEvent) => void): unknown {\n const TextStreamer = transformers.TextStreamer as unknown as new (tokenizer: unknown, options: Record<string, unknown>) => unknown;\n return new TextStreamer(tokenizer, {\n skip_prompt: true,\n skip_special_tokens: true,\n callback_function: (text: string) => { if (text) emit({ type: 'text', delta: text }); },\n });\n}\n\n
|
|
1
|
+
{"version":3,"file":"shared.js","sources":["../../src/runners/shared.ts"],"sourcesContent":["/**\n * What the two built-in runners share — kept in one place so the two cannot drift on\n * how progress is reported, how a stop reaches the model, or what a dropped tool turn\n * says. Types only from core (see `text-generation.ts` for why).\n */\n\nimport type { AparteStreamEvent } from '@aparte/core';\nimport type { RunnerContext, TransformersModule } from './types.js';\n\nexport const TOOL_TURNS_DROPPED =\n 'Dropped the tool call(s) and their results from the prompt: this runner does not support '\n + 'tool calling, so the model sees neither the call the assistant made nor the result that '\n + 'answered it. What the assistant SAID before calling stays in the prompt. Use an '\n + 'OpenAI-compatible endpoint for tools, or a runner that renders them.';\n\n/**\n * A content part neither built-in runner can carry. The union is text and image, so this\n * is unreachable from typed code — and reachable all the same from an app built against an\n * older aparte, which declared a third `file` part nothing ever filled. The wire mappers\n * guard the same case on the ROLE axis (`openai-compat`'s removed `tool_call`/`tool_result`);\n * the part axis had no guard, so the part was pushed into the image list as `undefined` and\n * `load_image` threw at generate time instead of the part being counted and named.\n */\nexport const UNSUPPORTED_PARTS_DROPPED =\n 'Dropped content part(s) this runner cannot carry: it takes text and image parts only, so '\n + 'anything else leaves the prompt with nothing in its place. A `file` part is the case to '\n + 'expect — it was removed from `AparteContentPart`; inline what you want the model to read '\n + 'as text, or send an image.';\n\n/**\n * The options a `from_pretrained` / `pipeline()` call takes from the context: download\n * progress forwarded to the page (percentages, rounded), dtype and device when set.\n */\nexport function loadOptions(ctx: RunnerContext): Record<string, unknown> {\n const opts: Record<string, unknown> = {\n progress_callback: (p: { status?: string; file?: string; progress?: number }) => {\n if (p.status === 'progress') ctx.progress({ status: 'downloading', file: p.file, progress: Math.round(p.progress ?? 0) });\n else if (p.status === 'done') ctx.progress({ status: 'loading', file: p.file });\n },\n };\n if (ctx.dtype) opts['dtype'] = ctx.dtype;\n if (ctx.device && ctx.device !== 'auto') opts['device'] = ctx.device;\n return opts;\n}\n\n/**\n * A stopping criteria the signal interrupts — so a Stop actually STOPS the model, not just\n * the read; otherwise generation runs to `max_new_tokens` off-thread, spending exactly the\n * CPU/GPU/battery this provider exists to save. Call `release()` in a `finally`.\n */\nexport function interruptOn(signal: AbortSignal, transformers: TransformersModule): { stopping: unknown; release(): void } {\n const stopping = new transformers.InterruptableStoppingCriteria();\n const onAbort = (): void => { stopping.interrupt(); };\n if (signal.aborted) onAbort();\n else signal.addEventListener('abort', onAbort, { once: true });\n return { stopping, release: () => { signal.removeEventListener('abort', onAbort); } };\n}\n\n/** A `TextStreamer` that emits each decoded token as a `text` event, prompt skipped. */\nexport function textStreamer(transformers: TransformersModule, tokenizer: unknown, emit: (event: AparteStreamEvent) => void): unknown {\n const TextStreamer = transformers.TextStreamer as unknown as new (tokenizer: unknown, options: Record<string, unknown>) => unknown;\n return new TextStreamer(tokenizer, {\n skip_prompt: true,\n skip_special_tokens: true,\n callback_function: (text: string) => { if (text) emit({ type: 'text', delta: text }); },\n });\n}\n\n/**\n * The reply's ceiling when no `maxTokens` is set: long enough that an answer is not cut\n * (some 6,000 words), short enough that a small model stuck repeating itself stops. Decoding\n * is greedy by default, and in the browser the attention cache grows with every token — a\n * loop left to run to a 131,072-token window ends in a GPU out-of-memory, not a clean stop.\n */\nexport const DEFAULT_MAX_NEW_TOKENS = 8192;\n\n/**\n * How many tokens the model's window holds, prompt and reply together, read from its\n * config — a vision model keeps it on the text half. `undefined` when the config says nothing.\n */\nexport function contextLength(config: unknown): number | undefined {\n const c = config as Record<string, unknown> | undefined;\n const text = c?.['text_config'] as Record<string, unknown> | undefined;\n for (const value of [c?.['max_position_embeddings'], text?.['max_position_embeddings'], c?.['n_positions']]) {\n if (typeof value === 'number' && value > 0) return value;\n }\n return undefined;\n}\n\n/**\n * Sampling options in Transformers.js' vocabulary, from the request's. The reply's ceiling is\n * the caller's `maxTokens`, else {@link DEFAULT_MAX_NEW_TOKENS} — always set: left out, the\n * text-generation pipeline would apply its own default of 256.\n */\nexport function generationOptions(options: { maxTokens?: number; temperature?: number }): Record<string, unknown> {\n const temperature = options.temperature ?? 0;\n return {\n max_new_tokens: options.maxTokens ?? DEFAULT_MAX_NEW_TOKENS,\n do_sample: temperature > 0,\n temperature: temperature > 0 ? temperature : undefined,\n };\n}\n\n/**\n * The stopping criteria a generate runs with: the Stop signal's, plus — when the model's\n * window is known — one that ends the reply when prompt and reply fill that window. A ceiling\n * larger than what the window has left would otherwise run the model past its context, where\n * it degrades; this bounds every ceiling, the default and the caller's, without having to\n * count the prompt's tokens first.\n */\nexport function stoppingCriteria(stopping: unknown, transformers: TransformersModule, contextSize: number | undefined): unknown {\n if (contextSize === undefined) return stopping;\n const MaxLengthCriteria = transformers.MaxLengthCriteria as unknown as new (maxLength: number) => unknown;\n return [stopping, new MaxLengthCriteria(contextSize)];\n}\n"],"names":[],"mappings":"AASO,MAAM,qBACT;AAaG,MAAM,4BACT;AASG,SAAS,YAAY,KAA6C;AACrE,QAAM,OAAgC;AAAA,IAClC,mBAAmB,CAAC,MAA6D;AAC7E,UAAI,EAAE,WAAW,gBAAgB,SAAS,EAAE,QAAQ,eAAe,MAAM,EAAE,MAAM,UAAU,KAAK,MAAM,EAAE,YAAY,CAAC,GAAG;AAAA,eAC/G,EAAE,WAAW,OAAQ,KAAI,SAAS,EAAE,QAAQ,WAAW,MAAM,EAAE,KAAA,CAAM;AAAA,IAClF;AAAA,EAAA;AAEJ,MAAI,IAAI,MAAO,MAAK,OAAO,IAAI,IAAI;AACnC,MAAI,IAAI,UAAU,IAAI,WAAW,OAAQ,MAAK,QAAQ,IAAI,IAAI;AAC9D,SAAO;AACX;AAOO,SAAS,YAAY,QAAqB,cAA0E;AACvH,QAAM,WAAW,IAAI,aAAa,8BAAA;AAClC,QAAM,UAAU,MAAY;AAAE,aAAS,UAAA;AAAA,EAAa;AACpD,MAAI,OAAO,QAAS,SAAA;AAAA,cACR,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM;AAC7D,SAAO,EAAE,UAAU,SAAS,MAAM;AAAE,WAAO,oBAAoB,SAAS,OAAO;AAAA,EAAG,EAAA;AACtF;AAGO,SAAS,aAAa,cAAkC,WAAoB,MAAmD;AAClI,QAAM,eAAe,aAAa;AAClC,SAAO,IAAI,aAAa,WAAW;AAAA,IAC/B,aAAa;AAAA,IACb,qBAAqB;AAAA,IACrB,mBAAmB,CAAC,SAAiB;AAAE,UAAI,KAAM,MAAK,EAAE,MAAM,QAAQ,OAAO,MAAM;AAAA,IAAG;AAAA,EAAA,CACzF;AACL;AAQO,MAAM,yBAAyB;AAM/B,SAAS,cAAc,QAAqC;AAC/D,QAAM,IAAI;AACV,QAAM,OAAO,IAAI,aAAa;AAC9B,aAAW,SAAS,CAAC,IAAI,yBAAyB,GAAG,OAAO,yBAAyB,GAAG,IAAI,aAAa,CAAC,GAAG;AACzG,QAAI,OAAO,UAAU,YAAY,QAAQ,EAAG,QAAO;AAAA,EACvD;AACA,SAAO;AACX;AAOO,SAAS,kBAAkB,SAAgF;AAC9G,QAAM,cAAc,QAAQ,eAAe;AAC3C,SAAO;AAAA,IACH,gBAAgB,QAAQ,aAAa;AAAA,IACrC,WAAW,cAAc;AAAA,IACzB,aAAa,cAAc,IAAI,cAAc;AAAA,EAAA;AAErD;AASO,SAAS,iBAAiB,UAAmB,cAAkC,aAA0C;AAC5H,MAAI,gBAAgB,OAAW,QAAO;AACtC,QAAM,oBAAoB,aAAa;AACvC,SAAO,CAAC,UAAU,IAAI,kBAAkB,WAAW,CAAC;AACxD;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-generation.d.ts","sourceRoot":"","sources":["../../src/runners/text-generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAuB,MAAM,YAAY,CAAC;AAGnF,KAAK,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhF,eAAO,MAAM,cAAc,QAGoB,CAAC;AAWhD,sEAAsE;AACtE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,aAAa,EAAE,CAoBlH;
|
|
1
|
+
{"version":3,"file":"text-generation.d.ts","sourceRoot":"","sources":["../../src/runners/text-generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAuB,MAAM,YAAY,CAAC;AAGnF,KAAK,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhF,eAAO,MAAM,cAAc,QAGoB,CAAC;AAWhD,sEAAsE;AACtE,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,aAAa,EAAE,CAoBlH;AAUD,eAAO,MAAM,YAAY,EAAE,YAsB1B,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as loadOptions, i as interruptOn, t as textStreamer, g as generationOptions, T as TOOL_TURNS_DROPPED } from "./shared.js";
|
|
1
|
+
import { l as loadOptions, c as contextLength, i as interruptOn, s as stoppingCriteria, t as textStreamer, g as generationOptions, T as TOOL_TURNS_DROPPED } from "./shared.js";
|
|
2
2
|
const IMAGES_DROPPED = 'This model has no vision runner: image parts were dropped from the prompt, so the model answers as if there were none. Register the model with task: "image-text-to-text", or point `runner` at a module of your own.';
|
|
3
3
|
function textOf(content) {
|
|
4
4
|
if (typeof content === "string") return content;
|
|
@@ -25,6 +25,7 @@ function flattenForChatTemplate(messages, warn) {
|
|
|
25
25
|
const createRunner = async (ctx) => {
|
|
26
26
|
const pipeline = ctx.transformers.pipeline;
|
|
27
27
|
const pipe = await pipeline("text-generation", ctx.modelId, loadOptions(ctx));
|
|
28
|
+
const contextSize = contextLength(pipe.model?.config);
|
|
28
29
|
return {
|
|
29
30
|
async generate({ messages, options, emit, signal }) {
|
|
30
31
|
const { stopping, release } = interruptOn(signal, ctx.transformers);
|
|
@@ -32,7 +33,7 @@ const createRunner = async (ctx) => {
|
|
|
32
33
|
await pipe(flattenForChatTemplate(messages, ctx.warn), {
|
|
33
34
|
...generationOptions(options),
|
|
34
35
|
streamer: textStreamer(ctx.transformers, pipe.tokenizer, emit),
|
|
35
|
-
stopping_criteria: stopping
|
|
36
|
+
stopping_criteria: stoppingCriteria(stopping, ctx.transformers, contextSize)
|
|
36
37
|
});
|
|
37
38
|
} finally {
|
|
38
39
|
release();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-generation.js","sources":["../../src/runners/text-generation.ts"],"sourcesContent":["/**\n * The built-in text runner — the generic `pipeline('text-generation')` path this\n * provider has always run, extracted from the worker so it is one runner among others.\n *\n * It flattens the conversation to `{ role, content: string }` turns (the tokenizer applies\n * the chat template). Two things it cannot carry, it SAYS: tool turns (their wire syntax is\n * model-specific) and image parts (a text model has no eyes). The second used to vanish\n * silently — a photo attached to a text model produced an answer that pretended — and\n * that silence, not the limitation, was the defect.\n */\n\n// Types only. A runner runs INSIDE the worker, and the worker bundle has no way to\n// resolve `@aparte/core` at runtime (an import map does not reach a worker) — so a value\n// import here is not externalised, it is inlined: the first build that imported\n// `contentToText` shipped all of core, components included, in a 426 kB runner chunk.\nimport type { AparteChatMessage, AparteContentPart } from '@aparte/core';\nimport type { CreateRunner, RunnerContext, RunnerGenerateInput } from './types.js';\nimport { TOOL_TURNS_DROPPED, generationOptions, interruptOn, loadOptions, textStreamer } from './shared.js';\n\ntype SimpleMessage = { role: 'user' | 'assistant' | 'system'; content: string };\n\nexport const IMAGES_DROPPED =\n 'This model has no vision runner: image parts were dropped from the prompt, so the model '\n + 'answers as if there were none. Register the model with task: \"image-text-to-text\", or '\n + 'point `runner` at a module of your own.';\n\n/** The text parts of a message, joined — what a text-only chat template can take. */\nfunction textOf(content: string | AparteContentPart[]): string {\n if (typeof content === 'string') return content;\n return content\n .filter((p): p is Extract<AparteContentPart, { type: 'text' }> => p.type === 'text')\n .map((p) => p.text)\n .join('');\n}\n\n/** Flatten to what the chat template takes; say what was left out. */\nexport function flattenForChatTemplate(messages: AparteChatMessage[], warn: RunnerContext['warn']): SimpleMessage[] {\n const result: SimpleMessage[] = [];\n let droppedImages = 0;\n let droppedToolTurns = 0;\n for (const m of messages) {\n if (m.role === 'user' || m.role === 'assistant' || m.role === 'system') {\n // An assistant's calls ride on an `assistant` message, which passes the role test\n // above and, when the model said nothing before them, carries no text either — so\n // without this the whole turn leaves the prompt with nothing said.\n if (m.toolCalls?.length) droppedToolTurns++;\n if (Array.isArray(m.content)) droppedImages += m.content.filter((p) => p.type === 'image').length;\n const text = textOf(m.content);\n if (text) result.push({ role: m.role, content: text });\n } else {\n droppedToolTurns++;\n }\n }\n if (droppedImages > 0) warn(IMAGES_DROPPED);\n if (droppedToolTurns > 0) warn(TOOL_TURNS_DROPPED);\n return result;\n}\n\n/** The pipeline, as this runner calls it. Transformers.js types it per task; this is the one task. */\ninterface TextPipeline {\n (messages: SimpleMessage[], options: Record<string, unknown>): Promise<unknown>;\n tokenizer: unknown;\n dispose?: () => Promise<void>;\n}\n\nexport const createRunner: CreateRunner = async (ctx) => {\n const pipeline = ctx.transformers.pipeline as unknown as (task: string, model: string, options: unknown) => Promise<TextPipeline>;\n const pipe = await pipeline('text-generation', ctx.modelId, loadOptions(ctx));\n\n return {\n async generate({ messages, options, emit, signal }: RunnerGenerateInput): Promise<void> {\n const { stopping, release } = interruptOn(signal, ctx.transformers);\n try {\n await pipe(flattenForChatTemplate(messages, ctx.warn), {\n ...generationOptions(options),\n streamer: textStreamer(ctx.transformers, pipe.tokenizer, emit),\n stopping_criteria: stopping,\n });\n } finally {\n release();\n }\n },\n dispose() {\n return pipe.dispose?.();\n },\n };\n};\n"],"names":[],"mappings":";AAqBO,MAAM,iBACT;AAKJ,SAAS,OAAO,SAA+C;AAC3D,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,SAAO,QACF,OAAO,CAAC,MAAyD,EAAE,SAAS,MAAM,EAClF,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,KAAK,EAAE;AAChB;AAGO,SAAS,uBAAuB,UAA+B,MAA8C;AAChH,QAAM,SAA0B,CAAA;AAChC,MAAI,gBAAgB;AACpB,MAAI,mBAAmB;AACvB,aAAW,KAAK,UAAU;AACtB,QAAI,EAAE,SAAS,UAAU,EAAE,SAAS,eAAe,EAAE,SAAS,UAAU;AAIpE,UAAI,EAAE,WAAW,OAAQ;AACzB,UAAI,MAAM,QAAQ,EAAE,OAAO,EAAG,kBAAiB,EAAE,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE;AAC3F,YAAM,OAAO,OAAO,EAAE,OAAO;AAC7B,UAAI,aAAa,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,MAAM;AAAA,IACzD,OAAO;AACH;AAAA,IACJ;AAAA,EACJ;AACA,MAAI,gBAAgB,EAAG,MAAK,cAAc;AAC1C,MAAI,mBAAmB,EAAG,MAAK,kBAAkB;AACjD,SAAO;AACX;
|
|
1
|
+
{"version":3,"file":"text-generation.js","sources":["../../src/runners/text-generation.ts"],"sourcesContent":["/**\n * The built-in text runner — the generic `pipeline('text-generation')` path this\n * provider has always run, extracted from the worker so it is one runner among others.\n *\n * It flattens the conversation to `{ role, content: string }` turns (the tokenizer applies\n * the chat template). Two things it cannot carry, it SAYS: tool turns (their wire syntax is\n * model-specific) and image parts (a text model has no eyes). The second used to vanish\n * silently — a photo attached to a text model produced an answer that pretended — and\n * that silence, not the limitation, was the defect.\n */\n\n// Types only. A runner runs INSIDE the worker, and the worker bundle has no way to\n// resolve `@aparte/core` at runtime (an import map does not reach a worker) — so a value\n// import here is not externalised, it is inlined: the first build that imported\n// `contentToText` shipped all of core, components included, in a 426 kB runner chunk.\nimport type { AparteChatMessage, AparteContentPart } from '@aparte/core';\nimport type { CreateRunner, RunnerContext, RunnerGenerateInput } from './types.js';\nimport { TOOL_TURNS_DROPPED, contextLength, generationOptions, interruptOn, loadOptions, stoppingCriteria, textStreamer } from './shared.js';\n\ntype SimpleMessage = { role: 'user' | 'assistant' | 'system'; content: string };\n\nexport const IMAGES_DROPPED =\n 'This model has no vision runner: image parts were dropped from the prompt, so the model '\n + 'answers as if there were none. Register the model with task: \"image-text-to-text\", or '\n + 'point `runner` at a module of your own.';\n\n/** The text parts of a message, joined — what a text-only chat template can take. */\nfunction textOf(content: string | AparteContentPart[]): string {\n if (typeof content === 'string') return content;\n return content\n .filter((p): p is Extract<AparteContentPart, { type: 'text' }> => p.type === 'text')\n .map((p) => p.text)\n .join('');\n}\n\n/** Flatten to what the chat template takes; say what was left out. */\nexport function flattenForChatTemplate(messages: AparteChatMessage[], warn: RunnerContext['warn']): SimpleMessage[] {\n const result: SimpleMessage[] = [];\n let droppedImages = 0;\n let droppedToolTurns = 0;\n for (const m of messages) {\n if (m.role === 'user' || m.role === 'assistant' || m.role === 'system') {\n // An assistant's calls ride on an `assistant` message, which passes the role test\n // above and, when the model said nothing before them, carries no text either — so\n // without this the whole turn leaves the prompt with nothing said.\n if (m.toolCalls?.length) droppedToolTurns++;\n if (Array.isArray(m.content)) droppedImages += m.content.filter((p) => p.type === 'image').length;\n const text = textOf(m.content);\n if (text) result.push({ role: m.role, content: text });\n } else {\n droppedToolTurns++;\n }\n }\n if (droppedImages > 0) warn(IMAGES_DROPPED);\n if (droppedToolTurns > 0) warn(TOOL_TURNS_DROPPED);\n return result;\n}\n\n/** The pipeline, as this runner calls it. Transformers.js types it per task; this is the one task. */\ninterface TextPipeline {\n (messages: SimpleMessage[], options: Record<string, unknown>): Promise<unknown>;\n tokenizer: unknown;\n model?: { config?: unknown };\n dispose?: () => Promise<void>;\n}\n\nexport const createRunner: CreateRunner = async (ctx) => {\n const pipeline = ctx.transformers.pipeline as unknown as (task: string, model: string, options: unknown) => Promise<TextPipeline>;\n const pipe = await pipeline('text-generation', ctx.modelId, loadOptions(ctx));\n const contextSize = contextLength(pipe.model?.config);\n\n return {\n async generate({ messages, options, emit, signal }: RunnerGenerateInput): Promise<void> {\n const { stopping, release } = interruptOn(signal, ctx.transformers);\n try {\n await pipe(flattenForChatTemplate(messages, ctx.warn), {\n ...generationOptions(options),\n streamer: textStreamer(ctx.transformers, pipe.tokenizer, emit),\n stopping_criteria: stoppingCriteria(stopping, ctx.transformers, contextSize),\n });\n } finally {\n release();\n }\n },\n dispose() {\n return pipe.dispose?.();\n },\n };\n};\n"],"names":[],"mappings":";AAqBO,MAAM,iBACT;AAKJ,SAAS,OAAO,SAA+C;AAC3D,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,SAAO,QACF,OAAO,CAAC,MAAyD,EAAE,SAAS,MAAM,EAClF,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,KAAK,EAAE;AAChB;AAGO,SAAS,uBAAuB,UAA+B,MAA8C;AAChH,QAAM,SAA0B,CAAA;AAChC,MAAI,gBAAgB;AACpB,MAAI,mBAAmB;AACvB,aAAW,KAAK,UAAU;AACtB,QAAI,EAAE,SAAS,UAAU,EAAE,SAAS,eAAe,EAAE,SAAS,UAAU;AAIpE,UAAI,EAAE,WAAW,OAAQ;AACzB,UAAI,MAAM,QAAQ,EAAE,OAAO,EAAG,kBAAiB,EAAE,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE;AAC3F,YAAM,OAAO,OAAO,EAAE,OAAO;AAC7B,UAAI,aAAa,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,MAAM;AAAA,IACzD,OAAO;AACH;AAAA,IACJ;AAAA,EACJ;AACA,MAAI,gBAAgB,EAAG,MAAK,cAAc;AAC1C,MAAI,mBAAmB,EAAG,MAAK,kBAAkB;AACjD,SAAO;AACX;AAUO,MAAM,eAA6B,OAAO,QAAQ;AACrD,QAAM,WAAW,IAAI,aAAa;AAClC,QAAM,OAAO,MAAM,SAAS,mBAAmB,IAAI,SAAS,YAAY,GAAG,CAAC;AAC5E,QAAM,cAAc,cAAc,KAAK,OAAO,MAAM;AAEpD,SAAO;AAAA,IACH,MAAM,SAAS,EAAE,UAAU,SAAS,MAAM,UAA8C;AACpF,YAAM,EAAE,UAAU,QAAA,IAAY,YAAY,QAAQ,IAAI,YAAY;AAClE,UAAI;AACA,cAAM,KAAK,uBAAuB,UAAU,IAAI,IAAI,GAAG;AAAA,UACnD,GAAG,kBAAkB,OAAO;AAAA,UAC5B,UAAU,aAAa,IAAI,cAAc,KAAK,WAAW,IAAI;AAAA,UAC7D,mBAAmB,iBAAiB,UAAU,IAAI,cAAc,WAAW;AAAA,QAAA,CAC9E;AAAA,MACL,UAAA;AACI,gBAAA;AAAA,MACJ;AAAA,IACJ;AAAA,IACA,UAAU;AACN,aAAO,KAAK,UAAA;AAAA,IAChB;AAAA,EAAA;AAER;"}
|
package/dist/runners/types.d.ts
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* what is not a generation (swapping an adapter, warming something up), reports progress
|
|
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
|
+
*
|
|
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.
|
|
15
19
|
*/
|
|
16
20
|
import type { AparteChatMessage, AparteStreamEvent } from '@aparte/core';
|
|
17
21
|
/** The Transformers.js module, as the worker resolved it — a runner never imports its own copy. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runners/types.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aparte/provider-transformers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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.
|
|
28
|
+
"@aparte/core": ">=0.20.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.
|
|
36
|
+
"@aparte/core": "0.20.0"
|
|
37
37
|
},
|
|
38
38
|
"keywords": [
|
|
39
39
|
"ai",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"dev": "vite",
|
|
59
|
-
"build": "vite build && tsc -b --emitDeclarationOnly --force",
|
|
59
|
+
"build": "vite build && tsc -b --emitDeclarationOnly --force && node ../../../../scripts/stamp-dist.mjs",
|
|
60
60
|
"preview": "vite preview",
|
|
61
61
|
"test": "vitest",
|
|
62
62
|
"test:run": "vitest run",
|