HearthNet / webagent /src /llm /webllm.js
GitHub Actions
feat: WebLLM browser agent with PeerJS mesh, HybridRAG, news signals, and easter-egg ticker
78cc96f
Raw
History Blame Contribute Delete
2.75 kB
// 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; } };
}