webbrain-compass-tiny-v2.1 / runtime /inference-worker.js
webbrain-one's picture
Release Compass Tiny v2.1: tested v24 q4f16 WebGPU, validation and caveats
3632834 verified
Raw
History Blame Contribute Delete
58.2 kB
/**
* Dedicated WebGPU worker for endpoint-free local model inference.
*
* WebGPU is unavailable in the MV3 service worker, and large ONNX allocations
* are more reliable in a dedicated worker than on the offscreen document's
* main thread. The offscreen host owns this worker and proxies correlated
* request/response messages to it.
*/
let libraryPromise = null;
let libraryVersion = null;
let workerConfig = null;
let visionRuntime = null;
let visionRuntimeKey = '';
let visionRuntimeModelKey = '';
let visionRuntimeOwner = '';
let visionRuntimeLoadPromise = null;
let visionRuntimeLoadKey = '';
let textRuntime = null;
let textRuntimeKey = '';
let textRuntimeModelKey = '';
let textRuntimeLoadPromise = null;
let textRuntimeLoadKey = '';
let modelOperationQueue = Promise.resolve();
const TRANSFORMERS_CACHE_NAME = 'transformers-cache';
const TEXT_DOWNLOAD_EVENT = 'text-download-state';
const WEBGPU_TEXT_MAX_NEW_TOKENS = 256;
const WEBGPU_LFM25_MODEL_ID = 'LiquidAI/LFM2.5-2.6B-ONNX';
const WEBGPU_LFM25_12B_INSTRUCT_MODEL_ID = 'LiquidAI/LFM2.5-1.2B-Instruct-ONNX';
const WEBGPU_LFM25_12B_THINKING_MODEL_ID = 'LiquidAI/LFM2.5-1.2B-Thinking-ONNX';
const WEBGPU_LFM25_VL_16B_MODEL_ID = 'LiquidAI/LFM2.5-VL-1.6B-ONNX';
const WEBGPU_LFM25_VL_3B_MODEL_ID = 'LiquidAI/LFM2.5-VL-3B-ONNX';
const WEBGPU_NANBEIGE42_3B_MODEL_ID = 'Michionlion/Nanbeige4.2-3B-ONNX-WebGPU';
const WEBGPU_MINICPM5_2B_MODEL_ID = 'RASMUS/MiniCPM5-2B-ONNX';
const WEBGPU_BONSAI27_MODEL_ID = 'prism-ml/Bonsai-27B-gguf';
const WEBGPU_LFM25_MAX_NEW_TOKENS = 2048;
const WEBGPU_LFM25_TEXT_MODEL_IDS = new Set([
WEBGPU_LFM25_MODEL_ID,
WEBGPU_LFM25_12B_INSTRUCT_MODEL_ID,
WEBGPU_LFM25_12B_THINKING_MODEL_ID,
]);
const WEBGPU_LFM25_VL_MODEL_IDS = new Set([
WEBGPU_LFM25_VL_16B_MODEL_ID,
WEBGPU_LFM25_VL_3B_MODEL_ID,
]);
// Presets whose reasoning is generated as hidden thinking rather than as part
// of the visible answer, so they need the long output budget.
const WEBGPU_REASONING_MODEL_IDS = new Set([
WEBGPU_LFM25_MODEL_ID,
WEBGPU_LFM25_12B_THINKING_MODEL_ID,
WEBGPU_NANBEIGE42_3B_MODEL_ID,
WEBGPU_MINICPM5_2B_MODEL_ID,
]);
// Chat templates that emit the opening `<think>` themselves, so the runtime
// only returns the reasoning suffix.
const WEBGPU_OPEN_THINKING_MODEL_IDS = new Set([
WEBGPU_LFM25_MODEL_ID,
WEBGPU_NANBEIGE42_3B_MODEL_ID,
]);
const WEBGPU_LONG_OUTPUT_MODEL_IDS = new Set([
...WEBGPU_LFM25_TEXT_MODEL_IDS,
WEBGPU_NANBEIGE42_3B_MODEL_ID,
WEBGPU_MINICPM5_2B_MODEL_ID,
]);
// Publisher-recommended decoding for the shipped reasoning presets. Custom
// repositories keep Transformers.js greedy defaults.
const WEBGPU_TEXT_SAMPLING = new Map([
[WEBGPU_LFM25_MODEL_ID, { temperature: 0.1, top_k: 50, repetition_penalty: 1.1 }],
[WEBGPU_LFM25_12B_THINKING_MODEL_ID, { temperature: 0.05, top_k: 50, repetition_penalty: 1.05 }],
// Nanbeige4.2-3B's own generation_config.json.
[WEBGPU_NANBEIGE42_3B_MODEL_ID, { temperature: 0.6, top_k: 20, top_p: 0.95 }],
// MiniCPM5-2B quickstart: temperature=1.0, top_p=0.95.
[WEBGPU_MINICPM5_2B_MODEL_ID, { temperature: 1.0, top_p: 0.95 }],
]);
// Nanbeige ships a single WebGPU-fused graph under a non-default file name.
// Without this override Transformers.js looks for `onnx/model_q4f16.onnx`,
// which the repository does not publish.
const WEBGPU_TEXT_MODEL_FILE_NAMES = new Map([
[WEBGPU_NANBEIGE42_3B_MODEL_ID, 'model_webgpu_mlp'],
]);
const WEBGPU_VISION_READY_MARKER_VERSION = 2;
const WEBGPU_VISION_READY_MARKER_PREFIX = 'https://webbrain.one/.well-known/webgpu-vision-ready/';
function createWebGpuTextSessionOptions() {
return {
extra: {
// ORT's default bucket cache can retain rounded-up transient buffers. That
// is especially costly for dynamic prefill/decode shapes on Metal.
'ep.webgpuexecutionprovider.storageBufferCacheMode': 'simple',
},
};
}
const readyTextModelKeys = new Set();
const textDownloadFiles = new Map();
const nativeFetch = typeof globalThis.fetch === 'function' ? globalThis.fetch.bind(globalThis) : null;
let activeTextDownloadModelId = '';
let queuedTextDownload = null;
let textDownloadAbortController = null;
let textDownloadCancelMode = '';
let activeVisionDownloadRequest = null;
let queuedVisionDownload = null;
let visionDownloadAbortController = null;
const activeVisionGenerations = new Map();
const queuedVisionGenerations = new Set();
const cancelledVisionGenerations = new Set();
let lastTextProgressPostAt = 0;
let webGpuAdapterProbePromise = null;
let webGpuAdapterSummary = '';
let observedWebGpuDevice = null;
let lastWebGpuDeviceError = '';
let lastWebGpuDeviceLost = '';
let textDownloadState = {
status: 'not-downloaded',
ready: false,
modelId: '',
dtype: '',
file: '',
loaded: 0,
total: 0,
progress: 0,
error: '',
};
function textDtypeKey(dtype) {
if (!dtype || typeof dtype !== 'object' || Array.isArray(dtype)) return String(dtype || '').trim();
return JSON.stringify(Object.fromEntries(
Object.entries(dtype).sort(([left], [right]) => left.localeCompare(right)),
));
}
function textModelKey(modelId, dtype) {
return `${String(modelId || '').trim()}|${textDtypeKey(dtype)}`;
}
function sameTextModel(leftModelId, leftDtype, rightModelId, rightDtype) {
return textModelKey(leftModelId, leftDtype) === textModelKey(rightModelId, rightDtype);
}
function assertOnnxTextModel(modelId) {
const normalized = String(modelId || '').trim();
if (!normalized) throw new Error('No text-generation model was specified.');
if (normalized === WEBGPU_BONSAI27_MODEL_ID || /\.gguf$/i.test(normalized)) {
throw new Error(`${normalized} is a GGUF checkpoint and cannot be loaded with Transformers.js. Use the Bonsai WebGPU runtime.`);
}
return normalized;
}
function isLfm25VlModel(modelId) {
return WEBGPU_LFM25_VL_MODEL_IDS.has(String(modelId || '').trim());
}
function lfm25VlProcessorOptions(modelId) {
if (!isLfm25VlModel(modelId)) return {};
return {
// LiquidAI's current VL ONNX repos use the Transformers v5 layout: image
// settings are nested in processor_config.json and the chat template is a
// separate Jinja file. The packaged Transformers.js 4.2 runtime supports
// both through WebBrain's documented browser-bundle compatibility hooks.
image_processor_config_file: 'processor_config.json',
chat_template_file: 'chat_template.jinja',
};
}
function assertTextDownloadCanStart(payload) {
const modelId = assertOnnxTextModel(payload?.modelId);
const dtype = payload?.dtype || 'q4f16';
const conflictsWithTransfer = textDownloadState.modelId
&& !sameTextModel(textDownloadState.modelId, textDownloadState.dtype, modelId, dtype)
&& ['downloading', 'paused', 'stopping'].includes(textDownloadState.status);
const conflictsWithQueued = queuedTextDownload
&& !sameTextModel(queuedTextDownload.modelId, queuedTextDownload.dtype, modelId, dtype);
if (conflictsWithTransfer || conflictsWithQueued) {
const blockingModel = conflictsWithTransfer ? textDownloadState.modelId : queuedTextDownload.modelId;
throw new Error(`Finish or stop the ${blockingModel} download before downloading ${modelId}.`);
}
return { modelId, dtype, key: textModelKey(modelId, dtype) };
}
function textReadyMarkerUrl(modelId, dtype) {
const key = encodeURIComponent(textModelKey(modelId, dtype));
return `https://webbrain.one/.well-known/webgpu-model-ready/${key}`;
}
function safeDecodedUrl(value) {
try { return decodeURIComponent(String(value || '')); } catch { return String(value || ''); }
}
function fetchTargetsActiveTextModel(input) {
if (!activeTextDownloadModelId) return false;
const url = typeof input === 'string' || input instanceof URL ? String(input) : input?.url;
return safeDecodedUrl(url).includes(`/${activeTextDownloadModelId}/`);
}
function fetchTargetsActiveVisionModel(input) {
if (!activeVisionDownloadRequest?.modelId) return false;
const url = typeof input === 'string' || input instanceof URL ? String(input) : input?.url;
return safeDecodedUrl(url).includes(`/${activeVisionDownloadRequest.modelId}/`);
}
function visionReadyMarkerUrl(modelId) {
return `${WEBGPU_VISION_READY_MARKER_PREFIX}v${WEBGPU_VISION_READY_MARKER_VERSION}/${encodeURIComponent(String(modelId || '').trim())}`;
}
function isVisionReadyMarkerForModel(url, modelId) {
const candidate = String(url || '');
const encodedModelId = encodeURIComponent(String(modelId || '').trim());
return candidate.startsWith(WEBGPU_VISION_READY_MARKER_PREFIX)
&& candidate.endsWith(`/${encodedModelId}`);
}
async function isVisionModelCached(modelId) {
const normalized = String(modelId || '').trim();
if (!normalized) return false;
if (typeof caches === 'undefined') return null;
const markerUrl = visionReadyMarkerUrl(normalized);
try {
for (const name of await caches.keys()) {
if (!/transformers/i.test(name)) continue;
const cache = await caches.open(name);
if (await cache.match(markerUrl)) return true;
}
} catch {
return null;
}
return false;
}
async function markVisionModelReady(modelId) {
const normalized = String(modelId || '').trim();
if (!normalized || typeof caches === 'undefined') return;
const cache = await caches.open(TRANSFORMERS_CACHE_NAME);
await cache.put(visionReadyMarkerUrl(normalized), new Response(JSON.stringify({
modelId: normalized,
markerVersion: WEBGPU_VISION_READY_MARKER_VERSION,
}), {
headers: { 'content-type': 'application/json' },
}));
}
async function controlledFetch(input, init = {}) {
if (!nativeFetch) throw new Error('Fetch is unavailable in the WebGPU worker.');
if (textDownloadAbortController && fetchTargetsActiveTextModel(input)) {
return nativeFetch(input, { ...init, signal: textDownloadAbortController.signal });
}
if (visionDownloadAbortController && fetchTargetsActiveVisionModel(input)) {
return nativeFetch(input, { ...init, signal: visionDownloadAbortController.signal });
}
return nativeFetch(input, init);
}
function textDownloadSnapshot() {
return { ...textDownloadState };
}
function postTextDownloadState({ force = false } = {}) {
const now = Date.now();
if (!force && now - lastTextProgressPostAt < 160) return;
lastTextProgressPostAt = now;
self.postMessage({ type: TEXT_DOWNLOAD_EVENT, state: textDownloadSnapshot() });
}
async function isTextModelReady(modelId, dtype) {
const key = textModelKey(modelId, dtype);
if (readyTextModelKeys.has(key)) return true;
if (typeof caches === 'undefined') return false;
try {
const cache = await caches.open(TRANSFORMERS_CACHE_NAME);
const marker = await cache.match(textReadyMarkerUrl(modelId, dtype));
if (!marker) return false;
readyTextModelKeys.add(key);
return true;
} catch {
return false;
}
}
async function markTextModelReady(modelId, dtype) {
const key = textModelKey(modelId, dtype);
if (typeof caches === 'undefined') {
readyTextModelKeys.add(key);
return;
}
const cache = await caches.open(TRANSFORMERS_CACHE_NAME);
await cache.put(textReadyMarkerUrl(modelId, dtype), new Response(JSON.stringify({ modelId, dtype }), {
headers: { 'content-type': 'application/json' },
}));
readyTextModelKeys.add(key);
}
async function loadLibrary() {
if (libraryPromise) return libraryPromise;
if (!workerConfig) throw new Error('WebGPU worker was not initialized.');
libraryPromise = (async () => {
let library;
try {
library = await import(workerConfig.transformersUrl);
} catch (error) {
libraryPromise = null;
throw new Error(`The packaged Transformers.js runtime could not be loaded: ${error?.message || error}`);
}
libraryVersion = library.env?.version || library.VERSION || 'unknown';
if (library.env) {
library.env.allowLocalModels = false;
library.env.allowRemoteModels = true;
library.env.useBrowserCache = true;
library.env.useWasmCache = false;
library.env.fetch = controlledFetch;
const wasm = library.env.backends?.onnx?.wasm;
if (wasm) {
wasm.numThreads = 1;
wasm.wasmPaths = {
mjs: workerConfig.wasmMjsUrl,
wasm: workerConfig.wasmUrl,
};
}
}
return library;
})();
return libraryPromise;
}
function compactAdapterInfo(adapter) {
if (!adapter) return '';
const info = adapter.info || {};
const identity = [info.vendor, info.architecture, info.device, info.description]
.map(value => String(value || '').trim())
.filter((value, index, values) => value && values.indexOf(value) === index)
.join(' / ');
const maxBufferSize = Number(adapter.limits?.maxBufferSize);
const maxStorageBinding = Number(adapter.limits?.maxStorageBufferBindingSize);
const limits = [
Number.isFinite(maxBufferSize) ? `maxBufferSize=${maxBufferSize}` : '',
Number.isFinite(maxStorageBinding) ? `maxStorageBufferBindingSize=${maxStorageBinding}` : '',
].filter(Boolean).join(', ');
return [identity, limits].filter(Boolean).join('; ');
}
async function captureWebGpuAdapterSummary() {
if (webGpuAdapterProbePromise) return webGpuAdapterProbePromise;
webGpuAdapterProbePromise = (async () => {
if (typeof navigator === 'undefined' || !navigator.gpu) return '';
try {
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
webGpuAdapterSummary = compactAdapterInfo(adapter);
} catch {}
return webGpuAdapterSummary;
})();
return webGpuAdapterProbePromise;
}
function bindWebGpuDeviceDiagnostics(library) {
const device = library?.env?.backends?.onnx?.webgpu?.device;
if (!device || device === observedWebGpuDevice) return;
observedWebGpuDevice = device;
lastWebGpuDeviceError = '';
lastWebGpuDeviceLost = '';
device.addEventListener?.('uncapturederror', event => {
lastWebGpuDeviceError = String(event?.error?.message || event?.message || 'Unknown WebGPU validation error.');
console.error('[webgpu] uncaptured device error:', lastWebGpuDeviceError);
});
device.lost?.then(info => {
if (device !== observedWebGpuDevice) return;
lastWebGpuDeviceLost = String(info?.message || info?.reason || 'The WebGPU device was lost.');
console.error('[webgpu] device lost:', lastWebGpuDeviceLost);
}).catch(() => {});
}
function isWebGpuExecutionFailure(error) {
return /OrtRun|BufferManager::Download|mapAsync|GPUBuffer|device lost/i.test(error?.message || String(error));
}
async function enrichWebGpuExecutionError(error) {
// WebGPU uncaptured-error/device-lost events can arrive just after OrtRun's
// generic buffer readback exception. Give that event one task to land so the
// user sees the actionable root error instead of only "Invalid Buffer".
await new Promise(resolve => setTimeout(resolve, 0));
const details = [lastWebGpuDeviceError, lastWebGpuDeviceLost]
.map(value => String(value || '').trim())
.filter((value, index, values) => value && values.indexOf(value) === index);
const adapter = webGpuAdapterSummary || await captureWebGpuAdapterSummary();
const suffix = [
details.length ? `GPU detail: ${details.join(' ')}` : '',
adapter ? `Adapter: ${adapter}.` : '',
'Close other GPU-heavy tabs/apps and retry with a short prompt. If it persists, this GPU/driver cannot execute this model with the current WebGPU runtime.',
].filter(Boolean).join(' ');
return new Error(`${error?.message || String(error)} ${suffix}`);
}
function postProgress(modelId, event) {
if (modelId === activeTextDownloadModelId && !textDownloadCancelMode) {
const file = String(event?.file || event?.name || '');
if (file) {
const previous = textDownloadFiles.get(file) || { loaded: 0, total: 0, status: '' };
const total = Number(event?.total || previous.total || 0);
const loaded = event?.status === 'done' && total > 0
? total
: Number(event?.loaded ?? previous.loaded ?? 0);
textDownloadFiles.set(file, {
status: event?.status || previous.status,
loaded: Math.max(0, loaded),
total: Math.max(0, total),
});
}
let loaded = 0;
let total = 0;
for (const item of textDownloadFiles.values()) {
if (item.total <= 0) continue;
loaded += Math.min(item.loaded, item.total);
total += item.total;
}
textDownloadState = {
...textDownloadState,
status: 'downloading',
ready: false,
file,
loaded,
total,
progress: total > 0 ? Math.max(0, Math.min(100, loaded / total * 100)) : 0,
error: '',
};
postTextDownloadState({ force: event?.status === 'done' });
}
self.postMessage({
type: 'progress',
modelId,
status: event?.status || '',
file: event?.file || event?.name || '',
loaded: Number(event?.loaded || 0),
total: Number(event?.total || 0),
progress: Number(event?.progress || 0),
});
}
async function disposeRuntime(runtime) {
if (runtime?.pipeline?.dispose) {
try { await runtime.pipeline.dispose(); } catch {}
} else if (runtime?.model?.dispose) {
try { await runtime.model.dispose(); } catch {}
}
if (runtime?.processor?.dispose) {
try { await runtime.processor.dispose(); } catch {}
}
}
async function disposeVisionRuntime(expectedOwner = '') {
if (expectedOwner && visionRuntimeOwner && visionRuntimeOwner !== expectedOwner) return;
const runtime = visionRuntime;
visionRuntime = null;
visionRuntimeKey = '';
visionRuntimeModelKey = '';
visionRuntimeOwner = '';
await disposeRuntime(runtime);
}
async function disposeTextRuntime() {
const runtime = textRuntime;
textRuntime = null;
textRuntimeKey = '';
textRuntimeModelKey = '';
await disposeRuntime(runtime);
}
async function disposeAllRuntimes() {
await disposeVisionRuntime();
await disposeTextRuntime();
}
async function legacyLfm25VlConfig(library, modelId, progress_callback, localFilesOnly) {
if (modelId !== WEBGPU_LFM25_VL_16B_MODEL_ID) return null;
if (!library.AutoConfig) {
throw new Error('The packaged Transformers.js version cannot load the LFM2.5-VL-1.6B model config.');
}
const config = await library.AutoConfig.from_pretrained(modelId, {
progress_callback,
local_files_only: localFilesOnly,
});
config['transformers.js_config'] = {
...(config['transformers.js_config'] || {}),
// LiquidAI's 1.6B ONNX export predates the standard Transformers.js
// ImageTextToText filenames used by the newer 3B package.
session_file_names: {
embed_tokens: 'embed_tokens',
vision_encoder: 'embed_images',
decoder_model_merged: 'decoder',
},
use_external_data_format: {
'embed_tokens_fp16.onnx': 1,
'embed_images_fp16.onnx': 1,
'decoder_q4.onnx': 1,
},
};
return config;
}
async function getVisionRuntime(modelId, dtype, device, {
localFilesOnly = false,
owner = 'vision',
readiness = 'vision',
} = {}) {
const key = `vision|${modelId}|${device}|${JSON.stringify(dtype)}`;
if (visionRuntime && visionRuntimeKey === key) {
visionRuntimeOwner = owner;
return visionRuntime;
}
if (visionRuntimeLoadPromise) {
if (visionRuntimeLoadKey === key) return visionRuntimeLoadPromise;
await visionRuntimeLoadPromise.catch(() => {});
if (visionRuntime && visionRuntimeKey === key) {
visionRuntimeOwner = owner;
return visionRuntime;
}
}
const loadPromise = (async () => {
const locallyReady = readiness === 'text'
? await isTextModelReady(modelId, dtype)
: await isVisionModelCached(modelId);
if (localFilesOnly && locallyReady === false) {
const error = new Error(`${modelId} is not cached locally.`);
error.code = readiness === 'text' ? 'text_model_not_downloaded' : 'vision_model_not_downloaded';
throw error;
}
const library = await loadLibrary();
const { AutoModelForImageTextToText, AutoProcessor } = library;
if (!AutoModelForImageTextToText || !AutoProcessor) {
throw new Error('The packaged Transformers.js version does not include image-text-to-text support.');
}
if (owner === 'text') await disposeTextRuntime();
await disposeVisionRuntime();
const progress_callback = event => postProgress(modelId, event);
const config = await legacyLfm25VlConfig(library, modelId, progress_callback, localFilesOnly);
const processorOptions = lfm25VlProcessorOptions(modelId);
const previousAllowLocalModels = library.env?.allowLocalModels;
if (localFilesOnly && library.env) library.env.allowLocalModels = true;
let processorResult;
let modelResult;
try {
[processorResult, modelResult] = await Promise.allSettled([
AutoProcessor.from_pretrained(modelId, {
...processorOptions,
progress_callback,
local_files_only: localFilesOnly,
}),
AutoModelForImageTextToText.from_pretrained(modelId, {
device,
dtype,
...(config ? { config } : {}),
progress_callback,
local_files_only: localFilesOnly,
}),
]);
} finally {
if (localFilesOnly && library.env) library.env.allowLocalModels = previousAllowLocalModels;
}
if (processorResult.status === 'rejected' || modelResult.status === 'rejected') {
const loaded = [processorResult, modelResult]
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
for (const resource of loaded) {
if (resource?.dispose) {
try { await resource.dispose(); } catch {}
}
}
throw processorResult.status === 'rejected'
? processorResult.reason
: modelResult.reason;
}
const processor = processorResult.value;
const model = modelResult.value;
visionRuntime = { library, processor, model };
visionRuntimeKey = key;
visionRuntimeModelKey = textModelKey(modelId, dtype);
visionRuntimeOwner = owner;
return visionRuntime;
})();
visionRuntimeLoadPromise = loadPromise;
visionRuntimeLoadKey = key;
try {
return await loadPromise;
} finally {
if (visionRuntimeLoadPromise === loadPromise) {
visionRuntimeLoadPromise = null;
visionRuntimeLoadKey = '';
}
}
}
async function preloadRuntime(payload = {}) {
const modelId = String(payload.modelId || '').trim();
if (!modelId) throw new Error('No vision model was specified.');
const device = payload.device || 'webgpu';
const dtype = payload.dtype || {
embed_tokens: 'fp16',
vision_encoder: 'fp16',
decoder_model_merged: 'q4',
};
await getVisionRuntime(modelId, dtype, device);
await disposeVisionRuntime();
return modelId;
}
function visionDownloadState(request, status) {
return {
status,
ready: status === 'ready',
modelId: request?.modelId || '',
dtype: request?.dtype || '',
};
}
async function preloadVisionModel(payload, request) {
if (queuedVisionDownload !== request || request.cancelMode) {
return visionDownloadState(request, request.cancelMode === 'stop' ? 'not-downloaded' : 'paused');
}
queuedVisionDownload = null;
activeVisionDownloadRequest = request;
const controller = new AbortController();
visionDownloadAbortController = controller;
try {
await preloadRuntime(payload);
const status = request.cancelMode === 'stop'
? 'not-downloaded'
: request.cancelMode === 'pause' ? 'paused' : 'ready';
if (status === 'ready') await markVisionModelReady(request.modelId);
return visionDownloadState(request, status);
} catch (error) {
if (request.cancelMode || controller.signal.aborted) {
return visionDownloadState(request, request.cancelMode === 'stop' ? 'not-downloaded' : 'paused');
}
throw error;
} finally {
if (visionDownloadAbortController === controller) visionDownloadAbortController = null;
if (activeVisionDownloadRequest === request) activeVisionDownloadRequest = null;
}
}
function pauseVisionDownload(modelId) {
const normalizedModelId = String(modelId || '').trim();
const queued = queuedVisionDownload;
const active = activeVisionDownloadRequest;
const targetQueued = queued && (!normalizedModelId || queued.modelId === normalizedModelId);
const targetActive = active && (!normalizedModelId || active.modelId === normalizedModelId);
if (targetQueued) {
queued.cancelMode = 'pause';
if (queuedVisionDownload === queued) queuedVisionDownload = null;
}
if (targetActive) {
active.cancelMode = 'pause';
visionDownloadAbortController?.abort();
}
const target = targetActive ? active : targetQueued ? queued : { modelId: normalizedModelId };
return {
...visionDownloadState(target, 'paused'),
targetsQueued: Boolean(targetQueued),
targetsActive: Boolean(targetActive),
};
}
function stopVisionDownload(modelId) {
const normalizedModelId = String(modelId || '').trim();
const queued = queuedVisionDownload;
const active = activeVisionDownloadRequest;
const targetsQueued = Boolean(queued && (!normalizedModelId || queued.modelId === normalizedModelId));
const targetsActive = Boolean(active && (!normalizedModelId || active.modelId === normalizedModelId));
if (targetsQueued) {
queued.cancelMode = 'stop';
if (queuedVisionDownload === queued) queuedVisionDownload = null;
}
if (targetsActive) {
active.cancelMode = 'stop';
visionDownloadAbortController?.abort();
}
return { targetsQueued, targetsActive, hasActiveVision: Boolean(active) };
}
async function getTextRuntime(modelId, dtype, device, { localFilesOnly = false } = {}) {
assertOnnxTextModel(modelId);
const key = `text|${modelId}|${device}|${JSON.stringify(dtype)}`;
if (textRuntime && textRuntimeKey === key) return textRuntime;
if (textRuntimeLoadPromise) {
if (textRuntimeLoadKey === key) return textRuntimeLoadPromise;
await textRuntimeLoadPromise.catch(() => {});
if (textRuntime && textRuntimeKey === key) return textRuntime;
}
const loadPromise = (async () => {
const library = await loadLibrary();
if (!library.pipeline) {
throw new Error('The packaged Transformers.js version does not include text generation.');
}
await captureWebGpuAdapterSummary();
await disposeVisionRuntime('text');
await disposeTextRuntime();
const previousAllowLocalModels = library.env?.allowLocalModels;
if (localFilesOnly && library.env) library.env.allowLocalModels = true;
let pipeline;
try {
pipeline = await library.pipeline('text-generation', modelId, {
device,
dtype,
...(WEBGPU_TEXT_MODEL_FILE_NAMES.has(modelId)
? { model_file_name: WEBGPU_TEXT_MODEL_FILE_NAMES.get(modelId) }
: {}),
// ORT mutates this object while appending its default session config.
session_options: createWebGpuTextSessionOptions(),
local_files_only: localFilesOnly,
progress_callback: event => postProgress(modelId, event),
});
} finally {
if (localFilesOnly && library.env) library.env.allowLocalModels = previousAllowLocalModels;
}
bindWebGpuDeviceDiagnostics(library);
textRuntime = {
library,
pipeline,
model: pipeline.model,
tokenizer: pipeline.tokenizer,
};
textRuntimeKey = key;
textRuntimeModelKey = textModelKey(modelId, dtype);
return textRuntime;
})();
textRuntimeLoadPromise = loadPromise;
textRuntimeLoadKey = key;
try {
return await loadPromise;
} finally {
if (textRuntimeLoadPromise === loadPromise) {
textRuntimeLoadPromise = null;
textRuntimeLoadKey = '';
}
}
}
async function getDownloadedTextRuntime(modelId, dtype, device, { localFilesOnly = false } = {}) {
if (isLfm25VlModel(modelId)) {
return getVisionRuntime(modelId, dtype, device, {
localFilesOnly,
owner: 'text',
readiness: 'text',
});
}
return getTextRuntime(modelId, dtype, device, { localFilesOnly });
}
async function disposeDownloadedTextRuntime(modelId = '') {
if (!modelId || isLfm25VlModel(modelId)) await disposeVisionRuntime('text');
if (!modelId || !isLfm25VlModel(modelId)) await disposeTextRuntime();
}
function chatTemplateText(value) {
if (typeof value === 'string') return value;
if (Array.isArray(value)) return value.map(chatTemplateText).join('\n');
if (value && typeof value === 'object') return Object.values(value).map(chatTemplateText).join('\n');
return '';
}
export function tokenizerSupportsTools(tokenizer) {
const template = chatTemplateText(tokenizer?.chat_template ?? tokenizer?.chatTemplate);
return /\btools\b/.test(template);
}
function assertToolCapableTextRuntime(runtime, modelId) {
if (tokenizerSupportsTools(runtime?.tokenizer)) return;
throw new Error(`${modelId} is not compatible with WebBrain: custom repositories must provide a chat template that accepts tools.`);
}
async function getTextDownloadStatus(modelId, dtype) {
const ready = await isTextModelReady(modelId, dtype);
const sameModel = sameTextModel(textDownloadState.modelId, textDownloadState.dtype, modelId, dtype);
if (sameModel && ['downloading', 'paused', 'stopping'].includes(textDownloadState.status)) {
return textDownloadSnapshot();
}
if (ready) {
return {
status: 'ready',
ready: true,
modelId,
dtype,
file: sameModel ? textDownloadState.file : '',
loaded: sameModel ? textDownloadState.loaded : 0,
total: sameModel ? textDownloadState.total : 0,
progress: 100,
error: '',
};
}
if (sameModel && textDownloadState.status === 'error') return textDownloadSnapshot();
return {
status: 'not-downloaded',
ready: false,
modelId,
dtype,
file: '',
loaded: 0,
total: 0,
progress: 0,
error: '',
};
}
async function downloadTextModel(payload, { onStarted } = {}) {
const modelId = assertOnnxTextModel(payload?.modelId);
const device = payload?.device || 'webgpu';
const dtype = payload?.dtype || 'q4f16';
const tracksDifferentTransfer = textDownloadState.modelId
&& !sameTextModel(textDownloadState.modelId, textDownloadState.dtype, modelId, dtype)
&& ['downloading', 'paused', 'stopping'].includes(textDownloadState.status);
if (tracksDifferentTransfer) {
throw new Error(`Finish or stop the ${textDownloadState.modelId} download before downloading ${modelId}.`);
}
await clearLegacyLfm25VlWrongPrecisionCache(modelId);
if (await isTextModelReady(modelId, dtype)) {
if (payload?.requireTools === true) {
const runtime = await getDownloadedTextRuntime(modelId, dtype, device, { localFilesOnly: true });
assertToolCapableTextRuntime(runtime, modelId);
}
textDownloadState = {
...textDownloadState,
status: 'ready',
ready: true,
modelId,
dtype,
progress: 100,
error: '',
};
postTextDownloadState({ force: true });
return textDownloadSnapshot();
}
const resuming = textDownloadState.status === 'paused'
&& sameTextModel(textDownloadState.modelId, textDownloadState.dtype, modelId, dtype);
if (!resuming) textDownloadFiles.clear();
activeTextDownloadModelId = modelId;
textDownloadCancelMode = '';
const controller = new AbortController();
textDownloadAbortController = controller;
textDownloadState = {
status: 'downloading',
ready: false,
modelId,
dtype,
file: resuming ? textDownloadState.file : '',
loaded: resuming ? textDownloadState.loaded : 0,
total: resuming ? textDownloadState.total : 0,
progress: resuming ? textDownloadState.progress : 0,
error: '',
};
postTextDownloadState({ force: true });
onStarted?.(textDownloadSnapshot());
try {
const runtime = await getDownloadedTextRuntime(modelId, dtype, device);
if (payload?.requireTools === true) assertToolCapableTextRuntime(runtime, modelId);
if (textDownloadCancelMode) {
await disposeDownloadedTextRuntime(modelId);
return textDownloadSnapshot();
}
await markTextModelReady(modelId, dtype);
textDownloadState = {
...textDownloadState,
status: 'ready',
ready: true,
progress: 100,
error: '',
};
postTextDownloadState({ force: true });
return textDownloadSnapshot();
} catch (error) {
if (textDownloadCancelMode === 'pause' || textDownloadCancelMode === 'stop' || controller.signal.aborted) {
textDownloadState = {
...textDownloadState,
status: textDownloadCancelMode === 'stop' ? 'stopping' : 'paused',
ready: false,
error: '',
};
postTextDownloadState({ force: true });
return textDownloadSnapshot();
}
textDownloadState = {
...textDownloadState,
status: 'error',
ready: false,
error: error?.message || String(error),
};
postTextDownloadState({ force: true });
throw error;
} finally {
if (textDownloadAbortController === controller) textDownloadAbortController = null;
activeTextDownloadModelId = '';
}
}
async function clearLegacyLfm25VlWrongPrecisionCache(modelId) {
if (modelId !== WEBGPU_LFM25_VL_16B_MODEL_ID || typeof caches === 'undefined') return 0;
const modelPath = `/${modelId}/`;
const wrongPrecisionFile = /\/onnx\/(?:decoder|embed_images)\.onnx(?:_data(?:_\d+)?)?(?:[?#]|$)/;
let deletedEntries = 0;
for (const name of await caches.keys()) {
if (!/transformers/i.test(name)) continue;
const cache = await caches.open(name);
for (const request of await cache.keys()) {
const url = safeDecodedUrl(request.url);
if (url.includes(modelPath) && wrongPrecisionFile.test(url) && await cache.delete(request)) {
deletedEntries++;
}
}
}
return deletedEntries;
}
function pauseTextDownload() {
if (textDownloadState.status !== 'downloading') return textDownloadSnapshot();
textDownloadCancelMode = 'pause';
textDownloadState = { ...textDownloadState, status: 'paused', ready: false, error: '' };
textDownloadAbortController?.abort();
postTextDownloadState({ force: true });
return textDownloadSnapshot();
}
async function clearTextModelCache(modelId, dtype) {
if (textRuntimeModelKey === textModelKey(modelId, dtype)) await disposeTextRuntime();
if (visionRuntimeOwner === 'text' && visionRuntimeModelKey === textModelKey(modelId, dtype)) {
await disposeVisionRuntime('text');
}
const modelPath = `/${modelId}/`;
const markerUrl = textReadyMarkerUrl(modelId, dtype);
if (typeof caches !== 'undefined') {
for (const name of await caches.keys()) {
if (!/transformers/i.test(name)) continue;
const cache = await caches.open(name);
for (const request of await cache.keys()) {
const url = safeDecodedUrl(request.url);
if (url.includes(modelPath) || request.url === markerUrl) await cache.delete(request);
}
await cache.delete(markerUrl);
}
}
readyTextModelKeys.delete(textModelKey(modelId, dtype));
textDownloadFiles.clear();
textDownloadCancelMode = '';
const clearedState = {
status: 'not-downloaded',
ready: false,
modelId,
dtype,
file: '',
loaded: 0,
total: 0,
progress: 0,
error: '',
};
const sameModel = !textDownloadState.modelId
|| sameTextModel(textDownloadState.modelId, textDownloadState.dtype, modelId, dtype);
if (sameModel) {
textDownloadState = clearedState;
postTextDownloadState({ force: true });
}
return { ...clearedState };
}
function enqueueModelOperation(operation) {
const result = modelOperationQueue.then(operation, operation);
// Keep the queue usable after one request fails while preserving that
// failure for the caller awaiting `result`.
modelOperationQueue = result.catch(() => {});
return result;
}
function imageUrlFromBlock(block) {
if (block?.type === 'image_url') {
return typeof block.image_url === 'string'
? block.image_url
: block.image_url?.url;
}
if (block?.type === 'image') {
return typeof block.image === 'string' ? block.image : block.url;
}
return '';
}
function prepareMultimodalMessages(messages) {
const imageUrls = [];
const prepared = [];
for (const message of Array.isArray(messages) ? messages : []) {
if (!message || typeof message !== 'object') continue;
const role = ['system', 'user', 'assistant', 'tool'].includes(message.role)
? message.role
: 'user';
const imageBlocks = [];
const textBlocks = [];
if (Array.isArray(message.content)) {
for (const block of message.content) {
if (block?.type === 'text' && typeof block.text === 'string') {
textBlocks.push({ type: 'text', text: block.text });
continue;
}
const imageUrl = imageUrlFromBlock(block);
if (imageUrl) {
imageUrls.push(imageUrl);
imageBlocks.push({ type: 'image' });
}
}
} else if (typeof message.content === 'string') {
textBlocks.push({ type: 'text', text: message.content });
}
// LFM2.5-VL's published chat template places <image> before the question.
// Normalize OpenAI-style messages (which often put text first) to that
// model-specific contract without changing the provider-facing API.
const blocks = [...imageBlocks, ...textBlocks];
if (blocks.length || Array.isArray(message.tool_calls)) {
prepared.push({
role,
content: blocks,
...(Array.isArray(message.tool_calls)
? { tool_calls: message.tool_calls.map(normalizeTextToolCall) }
: {}),
...(message.reasoning_content ? { reasoning_content: String(message.reasoning_content) } : {}),
});
}
}
return { messages: prepared, imageUrls };
}
function createVisionProbeImage(RawImage) {
if (!RawImage) throw new Error('The packaged runtime does not expose RawImage.');
// LFM2.5-VL-450M is much more dependable at coarse visual classification
// than fine OCR. Use three large, unlabeled color panels so the connection
// test still proves that pixels reached the model without asking it to read
// tiny synthetic glyphs.
const width = 480;
const height = 320;
const channels = 3;
const colors = [
[255, 255, 0],
[0, 0, 255],
[255, 0, 0],
];
const data = new Uint8ClampedArray(width * height * channels);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const targetOffset = (y * width + x) * channels;
const color = colors[Math.min(colors.length - 1, Math.floor(x / (width / colors.length)))];
for (let channel = 0; channel < channels; channel++) {
data[targetOffset + channel] = color[channel];
}
}
}
return new RawImage(data, width, height, channels);
}
async function runVision(payload, requestId) {
const modelId = String(payload?.modelId || '').trim();
if (!modelId) throw new Error('No vision model was specified.');
const device = payload?.device || 'webgpu';
const dtype = payload?.dtype || {
embed_tokens: 'fp16',
vision_encoder: 'fp16',
decoder_model_merged: 'q4',
};
const library = await loadLibrary();
const stoppingCriteria = library.InterruptableStoppingCriteria
? new library.InterruptableStoppingCriteria()
: null;
if (stoppingCriteria) activeVisionGenerations.set(requestId, stoppingCriteria);
try {
if (cancelledVisionGenerations.has(requestId)) {
const error = new Error('Vision generation was cancelled.');
error.name = 'AbortError';
throw error;
}
const runtime = await getVisionRuntime(modelId, dtype, device, {
localFilesOnly: true,
owner: 'vision',
readiness: 'vision',
});
const { messages, imageUrls } = prepareMultimodalMessages(payload?.messages);
if (imageUrls.length !== 1) {
throw new Error(`LFM2.5-VL requires exactly one screenshot; received ${imageUrls.length}.`);
}
const prompt = runtime.processor.apply_chat_template(messages, {
add_generation_prompt: true,
});
const image = payload?.options?.visionProbe === true
? createVisionProbeImage(runtime.library.RawImage)
: await runtime.library.load_image(imageUrls[0]);
const inputs = await runtime.processor(image, prompt, { add_special_tokens: false });
const requestedTokens = Number(payload?.options?.maxTokens);
const maxNewTokens = Number.isFinite(requestedTokens)
? Math.max(1, Math.min(1600, Math.round(requestedTokens)))
: 800;
const outputs = await runtime.model.generate({
...inputs,
do_sample: false,
max_new_tokens: maxNewTokens,
...(stoppingCriteria ? { stopping_criteria: [stoppingCriteria] } : {}),
});
if (cancelledVisionGenerations.has(requestId)) {
const error = new Error('Vision generation was cancelled.');
error.name = 'AbortError';
throw error;
}
const inputLength = inputs.input_ids.dims.at(-1);
const generated = outputs.slice(null, [inputLength, null]);
const decoded = runtime.processor.batch_decode(generated, { skip_special_tokens: true });
return String(decoded?.[0] || '').trim();
} finally {
activeVisionGenerations.delete(requestId);
cancelledVisionGenerations.delete(requestId);
}
}
function normalizeTextToolCall(toolCall) {
if (!toolCall || typeof toolCall !== 'object') return toolCall;
const usesFunctionWrapper = toolCall.function && typeof toolCall.function === 'object';
const target = usesFunctionWrapper ? toolCall.function : toolCall;
let parsedArguments = target.arguments;
if (typeof parsedArguments === 'string') {
try {
parsedArguments = JSON.parse(parsedArguments);
} catch {
parsedArguments = {};
}
}
if (!parsedArguments || typeof parsedArguments !== 'object' || Array.isArray(parsedArguments)) {
// Ling's template calls .items() unconditionally, so malformed or omitted
// historical arguments must still be represented by an object.
parsedArguments = {};
}
if (parsedArguments === target.arguments) return toolCall;
if (usesFunctionWrapper) {
return {
...toolCall,
function: { ...target, arguments: parsedArguments },
};
}
return { ...toolCall, arguments: parsedArguments };
}
export function prepareTextMessages(messages) {
return (Array.isArray(messages) ? messages : []).map(message => {
if (!message || typeof message !== 'object') return { role: 'user', content: '' };
const prepared = {
...message,
...(Array.isArray(message.tool_calls)
? { tool_calls: message.tool_calls.map(normalizeTextToolCall) }
: {}),
};
if (Array.isArray(message.content)) {
const text = message.content
.filter(block => block?.type === 'text' && typeof block.text === 'string')
.map(block => block.text)
.join('\n');
return { ...prepared, content: text };
}
return { ...prepared, content: String(message.content || '') };
});
}
export function splitThinking(content, { openingTagInPrompt = false } = {}) {
const source = String(content || '').trim();
const match = /^<think>\s*([\s\S]*?)\s*<\/think>\s*([\s\S]*)$/i.exec(source);
if (match) {
return {
content: String(match[2] || '').trim(),
reasoningContent: String(match[1] || '').trim() || null,
incompleteReasoning: false,
};
}
if (!openingTagInPrompt) {
return { content: source, reasoningContent: null, incompleteReasoning: false };
}
// LFM2.5's official template places `<think>` in the generation prompt.
// Transformers.js therefore returns only the generated suffix: reasoning,
// `</think>`, then the user-facing answer.
const closingTag = /<\/think>/i.exec(source);
if (!closingTag) {
return {
content: '',
reasoningContent: source || null,
incompleteReasoning: !!source,
};
}
return {
content: source.slice(closingTag.index + closingTag[0].length).trim(),
reasoningContent: source.slice(0, closingTag.index).trim() || null,
incompleteReasoning: false,
};
}
function addLegacyVlTools(messages, tools) {
if (!tools.length) return messages;
const toolText = `List of tools: [${tools.map(tool => JSON.stringify(tool)).join(', ')}]`;
const prepared = messages.map(message => ({ ...message, content: [...(message.content || [])] }));
if (prepared[0]?.role === 'system') {
prepared[0].content.push({ type: 'text', text: `\n${toolText}` });
} else {
prepared.unshift({ role: 'system', content: [{ type: 'text', text: toolText }] });
}
return prepared;
}
async function runMultimodalText(payload) {
const modelId = assertOnnxTextModel(payload?.modelId);
if (!isLfm25VlModel(modelId)) {
throw new Error(`${modelId} is not a shipped WebGPU multimodal model.`);
}
const device = payload?.device || 'webgpu';
const dtype = payload?.dtype || {
embed_tokens: 'fp16',
vision_encoder: 'fp16',
decoder_model_merged: 'q4',
};
if (!await isTextModelReady(modelId, dtype)) {
throw new Error(`${modelId} is not downloaded. Open Apocalypse Mode > WebGPU to download it before chatting.`);
}
const runtime = await getVisionRuntime(modelId, dtype, device, {
localFilesOnly: true,
owner: 'text',
readiness: 'text',
});
const tools = Array.isArray(payload?.options?.tools) ? payload.options.tools : [];
let { messages, imageUrls } = prepareMultimodalMessages(payload?.messages);
if (modelId === WEBGPU_LFM25_VL_16B_MODEL_ID) messages = addLegacyVlTools(messages, tools);
const prompt = runtime.processor.apply_chat_template(messages, {
add_generation_prompt: true,
tools: tools.length ? tools : undefined,
});
let inputs;
if (imageUrls.length) {
const images = await Promise.all(imageUrls.map(url => runtime.library.load_image(url)));
inputs = await runtime.processor(images.length === 1 ? images[0] : images, prompt, {
add_special_tokens: false,
});
} else {
inputs = runtime.processor.tokenizer(prompt, { add_special_tokens: false });
}
const requestedTokens = Number(payload?.options?.maxTokens);
const maxNewTokens = Number.isFinite(requestedTokens)
? Math.max(1, Math.min(1600, Math.round(requestedTokens)))
: 800;
lastWebGpuDeviceError = '';
lastWebGpuDeviceLost = '';
let outputs;
try {
outputs = await runtime.model.generate({
...inputs,
do_sample: false,
max_new_tokens: maxNewTokens,
});
} catch (error) {
if (isWebGpuExecutionFailure(error)) throw await enrichWebGpuExecutionError(error);
throw error;
}
const inputLength = inputs.input_ids.dims.at(-1);
const generated = outputs.slice(null, [inputLength, null]);
const decoded = runtime.processor.batch_decode(generated, { skip_special_tokens: true });
const result = splitThinking(String(decoded?.[0] || '').trim());
return { content: result.content, reasoningContent: result.reasoningContent };
}
async function runText(payload) {
const modelId = assertOnnxTextModel(payload?.modelId);
const device = payload?.device || 'webgpu';
const dtype = payload?.dtype || 'q4f16';
const usesReasoningTemplate = WEBGPU_REASONING_MODEL_IDS.has(modelId);
const opensThinkingInPrompt = WEBGPU_OPEN_THINKING_MODEL_IDS.has(modelId);
const usesLongOutputBudget = WEBGPU_LONG_OUTPUT_MODEL_IDS.has(modelId);
const sampling = WEBGPU_TEXT_SAMPLING.get(modelId);
if (!await isTextModelReady(modelId, dtype)) {
throw new Error(`${modelId} is not downloaded. Open Apocalypse Mode > WebGPU to download it before chatting.`);
}
const runtime = await getTextRuntime(modelId, dtype, device, { localFilesOnly: true });
if (payload?.requireTools === true) assertToolCapableTextRuntime(runtime, modelId);
const requestedTokens = Number(payload?.options?.maxTokens);
const maxTokenLimit = usesLongOutputBudget
? WEBGPU_LFM25_MAX_NEW_TOKENS
: WEBGPU_TEXT_MAX_NEW_TOKENS;
const maxNewTokens = Number.isFinite(requestedTokens)
? Math.max(1, Math.min(maxTokenLimit, Math.round(requestedTokens)))
: maxTokenLimit;
const tools = Array.isArray(payload?.options?.tools) ? payload.options.tools : [];
lastWebGpuDeviceError = '';
lastWebGpuDeviceLost = '';
let output;
try {
output = await runtime.pipeline(prepareTextMessages(payload?.messages), {
do_sample: Boolean(sampling),
...(sampling || {}),
max_new_tokens: maxNewTokens,
tools: tools.length ? tools : undefined,
// Reasoning templates re-open `<think>` for the pending turn and collapse
// completed thinking in history; the rest suppress thinking outright.
tokenizer_encode_kwargs: usesReasoningTemplate
? { preserve_thinking: false }
: { enable_thinking: false },
});
} catch (error) {
if (isWebGpuExecutionFailure(error)) throw await enrichWebGpuExecutionError(error);
throw error;
}
const generated = output?.[0]?.generated_text;
const content = Array.isArray(generated)
? generated.at(-1)?.content
: generated;
if (typeof content !== 'string') {
throw new Error('The WebGPU model returned no generated text.');
}
const result = splitThinking(content, { openingTagInPrompt: opensThinkingInPrompt });
if (result.incompleteReasoning) {
throw new Error(`${modelId} used its generation budget before finishing reasoning. Retry with a shorter prompt.`);
}
return { content: result.content, reasoningContent: result.reasoningContent };
}
async function probeRuntime() {
await loadLibrary();
const hasWebGPU = typeof navigator !== 'undefined' && !!navigator.gpu;
let adapter = null;
if (hasWebGPU) {
try { adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' }); } catch {}
}
const isFallbackAdapter = !!(adapter?.isFallbackAdapter ?? adapter?.info?.isFallbackAdapter);
return {
libraryVersion,
hasWebGPU: hasWebGPU && !!adapter,
isFallbackAdapter,
adapterFeatures: adapter ? [...adapter.features].slice(0, 12) : [],
};
}
export async function clearVisionModelCache(modelId) {
const normalizedModelId = String(modelId || '').trim();
if (!normalizedModelId) throw new Error('No vision model was specified.');
await disposeVisionRuntime('vision');
const modelPath = `/${normalizedModelId}/`;
const markerUrl = visionReadyMarkerUrl(normalizedModelId);
let deletedEntries = 0;
if (typeof caches !== 'undefined') {
for (const name of await caches.keys()) {
if (!/transformers/i.test(name)) continue;
const cache = await caches.open(name);
for (const request of await cache.keys()) {
const url = safeDecodedUrl(request.url);
if (url.includes(modelPath) || isVisionReadyMarkerForModel(request.url, normalizedModelId)) {
if (await cache.delete(request)) deletedEntries++;
}
}
if (await cache.delete(markerUrl)) deletedEntries++;
}
}
return { modelId: normalizedModelId, deletedEntries };
}
self.addEventListener('message', async event => {
const { id, type, payload } = event.data || {};
try {
if (type === 'cancel') {
const requestId = Number(payload?.requestId);
const queued = queuedVisionGenerations.has(requestId);
const active = activeVisionGenerations.has(requestId);
const cancellable = Number.isFinite(requestId) && (queued || active);
if (cancellable) {
cancelledVisionGenerations.add(requestId);
activeVisionGenerations.get(requestId)?.interrupt?.();
}
self.postMessage({
id,
ok: true,
cancelled: cancellable,
requestId,
queued: queued && !active,
active,
});
return;
}
if (type === 'init') {
workerConfig = payload;
self.postMessage({ id, ok: true });
return;
}
if (type === 'probe') {
self.postMessage({ id, ok: true, ...(await probeRuntime()) });
return;
}
if (type === 'text-download-status') {
const modelId = String(payload?.modelId || '').trim();
const dtype = payload?.dtype || 'q4f16';
self.postMessage({ id, ok: true, ...(await getTextDownloadStatus(modelId, dtype)) });
return;
}
if (type === 'download-text') {
const state = await enqueueModelOperation(() => downloadTextModel(payload));
self.postMessage({ id, ok: true, ...state });
return;
}
if (type === 'start-download-text') {
const request = assertTextDownloadCanStart(payload);
queuedTextDownload = request;
let acknowledged = false;
const operation = enqueueModelOperation(() => {
if (queuedTextDownload !== request) return getTextDownloadStatus(request.modelId, request.dtype);
return downloadTextModel(payload, {
onStarted(state) {
acknowledged = true;
self.postMessage({ id, ok: true, ...state });
},
});
});
void operation.then((state) => {
if (!acknowledged) self.postMessage({ id, ok: true, ...state });
}).catch((error) => {
if (!acknowledged) self.postMessage({ id, ok: false, error: error?.message || String(error) });
}).finally(() => {
if (queuedTextDownload?.key === request.key) queuedTextDownload = null;
});
return;
}
if (type === 'pause-text-download') {
self.postMessage({ id, ok: true, ...pauseTextDownload() });
return;
}
if (type === 'stop-text-download') {
const modelId = String(payload?.modelId || '').trim();
const dtype = payload?.dtype || 'q4f16';
const targetsQueuedTransfer = queuedTextDownload
&& sameTextModel(queuedTextDownload.modelId, queuedTextDownload.dtype, modelId, dtype);
if (targetsQueuedTransfer) queuedTextDownload = null;
const targetsTrackedTransfer = sameTextModel(textDownloadState.modelId, textDownloadState.dtype, modelId, dtype);
if (targetsTrackedTransfer) {
textDownloadCancelMode = 'stop';
textDownloadState = { ...textDownloadState, status: 'stopping', ready: false, error: '' };
if (activeTextDownloadModelId === modelId) textDownloadAbortController?.abort();
postTextDownloadState({ force: true });
}
const state = await enqueueModelOperation(() => clearTextModelCache(modelId, dtype));
self.postMessage({ id, ok: true, ...state });
return;
}
if (type === 'pause-vision-download') {
self.postMessage({ id, ok: true, ...pauseVisionDownload(payload?.modelId) });
return;
}
if (type === 'stop-vision-download') {
const modelId = String(payload?.modelId || '').trim();
if (!modelId) throw new Error('No vision model was specified.');
const stopped = stopVisionDownload(modelId);
// A queued preload owns no live vision operation. Clear its model-specific
// cache immediately so Stop is not trapped behind an unrelated text-model
// transfer in the shared WebGPU operation queue. The host may also pass
// targetsQueued after pause already dequeued that preload.
const targetsQueued = stopped.targetsQueued || payload?.targetsQueued === true;
const result = targetsQueued && !stopped.hasActiveVision
? await clearVisionModelCache(modelId)
: await enqueueModelOperation(() => clearVisionModelCache(modelId));
self.postMessage({
id,
ok: true,
status: 'not-downloaded',
ready: false,
...result,
});
return;
}
if (type === 'clear-cache') {
const modelId = String(payload?.modelId || '').trim();
const result = await enqueueModelOperation(() => clearVisionModelCache(modelId));
self.postMessage({ id, ok: true, ...result });
return;
}
if (type === 'dispose' || type === 'dispose-all') {
await enqueueModelOperation(disposeAllRuntimes);
self.postMessage({ id, ok: true, disposed: true });
return;
}
if (type === 'dispose-vision') {
await enqueueModelOperation(() => disposeVisionRuntime('vision'));
self.postMessage({ id, ok: true, disposed: true });
return;
}
if (type === 'dispose-text') {
await enqueueModelOperation(() => disposeDownloadedTextRuntime());
self.postMessage({ id, ok: true, disposed: true });
return;
}
if (type === 'preload') {
const modelId = String(payload?.modelId || '').trim();
if (!modelId) throw new Error('No vision model was specified.');
const request = {
modelId,
dtype: payload?.dtype || '',
cancelMode: '',
};
queuedVisionDownload = request;
self.postMessage({ type: 'vision-preload-state', modelId, status: 'queued' });
const state = await enqueueModelOperation(() => {
self.postMessage({ type: 'vision-preload-state', modelId, status: 'loading' });
return preloadVisionModel(payload, request);
});
if (queuedVisionDownload === request) queuedVisionDownload = null;
self.postMessage({ id, ok: true, ...state });
return;
}
if (type === 'chat') {
queuedVisionGenerations.add(id);
try {
const content = await enqueueModelOperation(() => runVision(payload, id));
self.postMessage({ id, ok: true, content, raw: { model: payload?.modelId || '' } });
} finally {
queuedVisionGenerations.delete(id);
cancelledVisionGenerations.delete(id);
}
return;
}
if (type === 'text-chat') {
const result = await enqueueModelOperation(() => runText(payload));
self.postMessage({
id,
ok: true,
...result,
raw: { model: payload?.modelId || '' },
});
return;
}
if (type === 'multimodal-text-chat') {
const result = await enqueueModelOperation(() => runMultimodalText(payload));
self.postMessage({
id,
ok: true,
...result,
raw: { model: payload?.modelId || '' },
});
return;
}
throw new Error(`Unknown WebGPU worker message: ${type || 'missing type'}`);
} catch (error) {
self.postMessage({ id, ok: false, error: error?.message || String(error) });
}
});