import { selectEmbeddingPrecision } from '../model-precision.js'; /** * Format-neutral runtime boundary. * * An ONNX or GGUF implementation must satisfy the same adapter contract. The * product UI intentionally has no dependency on either runtime. */ class Runtime extends EventTarget { adapter = null; status = 'idle'; backend = 'Engine not selected'; webgpu = null; embeddingPrecision = null; emit(type, detail) { this.dispatchEvent(new CustomEvent(type, { detail })); } log(level, message, detail = '') { if (level !== 'error' && level !== 'warn') return; console[level](`[LFM edge runtime] ${message}`, detail || ''); } async probe() { if (!globalThis.isSecureContext) throw new Error('WebGPU requires HTTPS or localhost.'); if (!navigator.gpu) throw new Error('WebGPU is unavailable in this browser.'); const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' }); if (!adapter) throw new Error('The browser did not return a WebGPU adapter.'); const info = adapter.info || {}; this.webgpu = { adapter, name: info.description || info.device || info.architecture || 'WebGPU adapter', vendor: info.vendor || 'Not exposed', architecture: info.architecture || 'Not exposed', features: [...adapter.features].sort(), limits: adapter.limits, }; return this.webgpu; } async load() { if (this.status === 'loading') return; this.status = 'loading'; this.emit('status', { status: this.status, backend: this.backend }); try { await this.probe(); const baseManifest = await fetch('/model-manifest.json', { cache: 'no-store' }).then(response => response.json()); const selection = selectEmbeddingPrecision(baseManifest, this.webgpu.features); const manifest = selection.manifest; this.embeddingPrecision = selection.precision; this.emit('status', { status: this.status, backend: this.backend }); const loaders = { 'onnx-transformers': () => import('../engines/onnx-transformers-engine.js'), }; const loadEngine = loaders[manifest.engine?.adapter]; if (!loadEngine) throw new Error(`Unknown or unselected inference adapter: ${manifest.engine?.adapter || 'none'}.`); const module = await loadEngine(); this.adapter = await module.createEngine({ manifest, telemetry: event => this.log(event.level, event.message, event.detail) }); await this.adapter.load(progress => this.emit('progress', progress)); this.backend = this.adapter.backend; this.status = 'ready'; } catch (error) { this.status = 'error'; this.log('error', 'Engine load stopped', error.message); throw error; } finally { this.emit('status', { status: this.status, backend: this.backend }); } } /** Returns { text, toolCalls, finishReason } for every inference adapter. */ async generate(messages, options) { if (!this.adapter || this.status !== 'ready') throw new Error('No inference engine is configured.'); this.status = 'generating'; this.emit('status', { status: this.status, backend: this.backend }); try { return await this.adapter.generate(messages, options); } finally { this.status = 'ready'; this.emit('status', { status: this.status, backend: this.backend }); } } async clearCache() { const result = this.adapter?.clearCache ? await this.adapter.clearCache() : { cleared: false, entriesDeleted: 0 }; const cleared = typeof result === 'object' ? result.cleared : Boolean(result); return cleared; } async cacheInfo() { if (this.adapter?.cacheInfo) return this.adapter.cacheInfo(); if (globalThis.caches) { const cacheNames = await caches.keys(); const modelCacheNames = cacheNames.filter(name => name === 'liquid-lfm-models-v4'); let used = 0; for (const cacheName of modelCacheNames) { const cache = await caches.open(cacheName); for (const request of await cache.keys()) { const response = await cache.match(request); used += Number(response?.headers.get('content-length') || 0); } } const estimate = await navigator.storage?.estimate?.(); return { used, available: estimate?.quota || 0 }; } const estimate = await navigator.storage?.estimate?.(); return { used: 0, available: estimate?.quota || 0 }; } clearConversationCache() { this.adapter?.clearConversationCache?.(); } } export const runtime = new Runtime();