Spaces:
Running on Zero
Running on Zero
GitHub Actions
feat: WebLLM browser agent with PeerJS mesh, HybridRAG, news signals, and easter-egg ticker
78cc96f | // src/llm/webllm.js | |
| // Browser-local LLM via @mlc-ai/web-llm (WebGPU). No server, no API keys. | |
| // Models are downloaded once and cached in the browser (IndexedDB / Cache API). | |
| import * as webllm from "https://esm.run/@mlc-ai/web-llm"; | |
| // Small, browser-friendly instruct models. First is the default. | |
| export const MODELS = [ | |
| { id: "SmolLM2-360M-Instruct-q4f16_1-MLC", label: "SmolLM2 360M (smallest, fastest)" }, | |
| { id: "Qwen2.5-0.5B-Instruct-q4f16_1-MLC", label: "Qwen2.5 0.5B" }, | |
| { id: "Llama-3.2-1B-Instruct-q4f32_1-MLC", label: "Llama 3.2 1B (best quality)" }, | |
| ]; | |
| export function hasWebGPU() { | |
| return typeof navigator !== "undefined" && "gpu" in navigator; | |
| } | |
| export function createWebLLM({ onProgress } = {}) { | |
| let engine = null; | |
| let currentModel = null; | |
| let loading = null; | |
| async function ensure(modelId) { | |
| const id = modelId || MODELS[0].id; | |
| if (engine && currentModel === id) return engine; | |
| if (loading) await loading.catch(() => {}); | |
| if (!hasWebGPU()) { | |
| throw new Error( | |
| "WebGPU is not available in this browser. Use a recent Chrome/Edge (or enable WebGPU) to run the local model." | |
| ); | |
| } | |
| loading = (async () => { | |
| onProgress?.({ stage: "init", model: id, text: `Loading ${id}…` }); | |
| engine = await webllm.CreateMLCEngine(id, { | |
| initProgressCallback: (p) => onProgress?.({ stage: "download", model: id, text: p.text, progress: p.progress }), | |
| }); | |
| currentModel = id; | |
| onProgress?.({ stage: "ready", model: id, text: `Model ready: ${id}` }); | |
| return engine; | |
| })(); | |
| return loading; | |
| } | |
| // Matches the runtime contract: chat({ messages, stream, onToken, temperature, max_tokens, signal }). | |
| async function chat({ messages, stream = true, onToken, temperature = 0.4, max_tokens = 900, model, signal }) { | |
| const eng = await ensure(model); | |
| let text = ""; | |
| if (stream) { | |
| const reply = await eng.chat.completions.create({ | |
| messages, | |
| temperature, | |
| max_tokens, | |
| stream: true, | |
| }); | |
| for await (const chunk of reply) { | |
| if (signal?.aborted) break; | |
| const delta = chunk.choices?.[0]?.delta?.content || ""; | |
| if (delta) { | |
| text += delta; | |
| onToken?.(delta); | |
| } | |
| } | |
| return { text }; | |
| } | |
| const res = await eng.chat.completions.create({ messages, temperature, max_tokens, stream: false }); | |
| text = res.choices?.[0]?.message?.content || ""; | |
| return { text }; | |
| } | |
| async function complete(prompt, opts = {}) { | |
| const { text } = await chat({ messages: [{ role: "user", content: prompt }], stream: false, ...opts }); | |
| return text; | |
| } | |
| return { chat, complete, ensure, get model() { return currentModel; } }; | |
| } | |