Spaces:
Running
Running
| 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(); | |