LiquidAI/LFM2.5-VL-3B-WebGPU
36
1import { selectEmbeddingPrecision } from '../model-precision.js';2 3/**4 * Format-neutral runtime boundary.5 *6 * An ONNX or GGUF implementation must satisfy the same adapter contract. The7 * product UI intentionally has no dependency on either runtime.8 */9class Runtime extends EventTarget {10 adapter = null;11 status = 'idle';12 backend = 'Engine not selected';13 webgpu = null;14 embeddingPrecision = null;15 16 emit(type, detail) {17 this.dispatchEvent(new CustomEvent(type, { detail }));18 }19 20 log(level, message, detail = '') {21 if (level !== 'error' && level !== 'warn') return;22 console[level](`[LFM edge runtime] ${message}`, detail || '');23 }24 25 async probe() {26 if (!globalThis.isSecureContext) throw new Error('WebGPU requires HTTPS or localhost.');27 if (!navigator.gpu) throw new Error('WebGPU is unavailable in this browser.');28 const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });29 if (!adapter) throw new Error('The browser did not return a WebGPU adapter.');30 const info = adapter.info || {};31 this.webgpu = {32 adapter,33 name: info.description || info.device || info.architecture || 'WebGPU adapter',34 vendor: info.vendor || 'Not exposed',35 architecture: info.architecture || 'Not exposed',36 features: [...adapter.features].sort(),37 limits: adapter.limits,38 };39 return this.webgpu;40 }41 42 async load() {43 if (this.status === 'loading') return;44 this.status = 'loading';45 this.emit('status', { status: this.status, backend: this.backend });46 try {47 await this.probe();48 const baseManifest = await fetch('/model-manifest.json', { cache: 'no-store' }).then(response => response.json());49 const selection = selectEmbeddingPrecision(baseManifest, this.webgpu.features);50 const manifest = selection.manifest;51 this.embeddingPrecision = selection.precision;52 this.emit('status', { status: this.status, backend: this.backend });53 const loaders = {54 'onnx-transformers': () => import('../engines/onnx-transformers-engine.js'),55 };56 const loadEngine = loaders[manifest.engine?.adapter];57 if (!loadEngine) throw new Error(`Unknown or unselected inference adapter: ${manifest.engine?.adapter || 'none'}.`);58 const module = await loadEngine();59 this.adapter = await module.createEngine({ manifest, telemetry: event => this.log(event.level, event.message, event.detail) });60 await this.adapter.load(progress => this.emit('progress', progress));61 this.backend = this.adapter.backend;62 this.status = 'ready';63 } catch (error) {64 this.status = 'error';65 this.log('error', 'Engine load stopped', error.message);66 throw error;67 } finally {68 this.emit('status', { status: this.status, backend: this.backend });69 }70 }71 72 /** Returns { text, toolCalls, finishReason } for every inference adapter. */73 async generate(messages, options) {74 if (!this.adapter || this.status !== 'ready') throw new Error('No inference engine is configured.');75 this.status = 'generating';76 this.emit('status', { status: this.status, backend: this.backend });77 try {78 return await this.adapter.generate(messages, options);79 } finally {80 this.status = 'ready';81 this.emit('status', { status: this.status, backend: this.backend });82 }83 }84 85 async clearCache() {86 const result = this.adapter?.clearCache ? await this.adapter.clearCache() : { cleared: false, entriesDeleted: 0 };87 const cleared = typeof result === 'object' ? result.cleared : Boolean(result);88 return cleared;89 }90 91 async cacheInfo() {92 if (this.adapter?.cacheInfo) return this.adapter.cacheInfo();93 if (globalThis.caches) {94 const cacheNames = await caches.keys();95 const modelCacheNames = cacheNames.filter(name => name === 'liquid-lfm-models-v4');96 let used = 0;97 for (const cacheName of modelCacheNames) {98 const cache = await caches.open(cacheName);99 for (const request of await cache.keys()) {100 const response = await cache.match(request);101 used += Number(response?.headers.get('content-length') || 0);102 }103 }104 const estimate = await navigator.storage?.estimate?.();105 return { used, available: estimate?.quota || 0 };106 }107 const estimate = await navigator.storage?.estimate?.();108 return { used: 0, available: estimate?.quota || 0 };109 }110 111 clearConversationCache() {112 this.adapter?.clearConversationCache?.();113 }114}115 116export const runtime = new Runtime();117 