| import { selectEmbeddingPrecision } from '../model-precision.js'; |
|
|
| |
| |
| |
| |
| |
| |
| 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 }); |
| } |
| } |
|
|
| |
| 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(); |
|
|