// llm.js-style wrapper around latest llama.cpp compiled to WASM. // // The public API is the one from rahuldshetty/llm.js: // new LLM(type, url, init_callback, write_result_callback, on_complete_callback) // app.load_worker(); app.run({ prompt, ... }); // // Additions (all optional): // on_error_callback(err), on_progress_callback({ phase, loaded, total, ... }), // on_log_callback(line), on_event(message) // app.add_url_model({ url, name, expected_sha256 }) // app.add_file_model({ file, name, keep_copy }) // app.pause() / app.resume() / app.delete_model(id) / app.list_models() // // Deviations from the original: models live in a local library (uuid record per // model, see storage.js), so `url` became a target object { id, url, name }. A // plain url string is still accepted and treated as `{ url }`. // // The two ingest paths (url, local file) produce the same record and are loaded // by the same code path; only where the bytes come from differs. import { action } from "./actions.js"; export const MODELS = { GGUF_CPU: "GGUF_CPU", }; class LLM { constructor( type, target, init_callback, write_result_callback, on_complete_callback, on_error_callback, on_progress_callback, on_log_callback, ) { this.type = type; this.model = typeof target === "string" ? { url: target } : (target || {}); this.init_callback = init_callback; this.write_result_callback = write_result_callback; this.on_complete_callback = on_complete_callback; this.on_error_callback = on_error_callback; this.on_progress_callback = on_progress_callback; this.on_log_callback = on_log_callback; this.on_event = null; // any message, e.g. PAUSED / DELETED / MODELS this.worker = null; this.model_loaded = false; this.context_size = 2048; this._pending = null; } get url() { return this.model.url; } // autoload: post LOAD for this.model right away (the original behaviour); // pass false to only start the worker and drive loads yourself load_worker(options = {}) { switch (this.type) { case MODELS.GGUF_CPU: this.worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module" }); break; default: throw new Error(`unknown model type: ${this.type}`); } this.context_size = options.context_size || 2048; this.worker.onmessage = (event) => { const data = event.data; if (this.on_event) this.on_event(data); switch (data.event) { case action.INITIALIZED: this.model_loaded = true; if (this.init_callback) this.init_callback(data); this._flushPending(); break; case action.WRITE_RESULT: if (this.write_result_callback) this.write_result_callback(data.line); break; case action.RUN_COMPLETED: if (this.on_complete_callback) this.on_complete_callback(data); break; case action.PROGRESS: if (this.on_progress_callback) this.on_progress_callback(data); break; case action.LOG: if (this.on_log_callback) this.on_log_callback(data.line); break; case action.ERROR: if (this.on_error_callback) this.on_error_callback(data.error); else console.error("llm.js:", data.error); break; } }; const autoload = options.autoload !== false; if (autoload && (this.model.id || this.model.url)) { this.worker.postMessage({ event: action.LOAD, id: this.model.id || null, url: this.model.url || null, context_size: this.context_size, }); } } post(message) { if (this.worker) this.worker.postMessage(message); } // download a GGUF and add it to the library (then load it) add_url_model({ url, name = "", expected_sha256 = "", context_size = 0 } = {}) { this.post({ event: action.ADD_URL, url, name, expectedSha256: expected_sha256, context_size: context_size || this.context_size, }); } // register a File the user already has; keep_copy decides whether the bytes // are duplicated into IndexedDB (default: no, read them in place) add_file_model({ file, name = "", keep_copy = false, expected_sha256 = "", context_size = 0 } = {}) { this.post({ event: action.ADD_FILE, file, name, keepCopy: keep_copy, expectedSha256: expected_sha256, context_size: context_size || this.context_size, }); } // load a model from the library by id load(id, file = null) { this.post({ event: action.LOAD, id, file, context_size: this.context_size, }); } // free the engine and load the same model again reload(id) { this.post({ event: action.RELOAD_MODEL, id: id || this.model.id, context_size: this.context_size }); } // render a prompt through a chat template without generating; the result // arrives as a TEMPLATE_PREVIEW event preview_template({ prompt, template_source = "", messages = null, tools = "[]", template_kwargs = "{}" } = {}) { this.post({ event: action.PREVIEW_TEMPLATE, prompt, template_source, messages, tools, template_kwargs, }); } // stop a running generation; the text streamed so far is kept and the run // completes with interrupted: true stop() { this.post({ event: action.STOP }); } // resume an interrupted download / reload the same model resume() { this.post({ event: action.RESUME }); } pause() { this.post({ event: action.PAUSE }); } // drop a model (or the current one) from the library delete_model(id) { const target = id || this.model.id; if (target) this.post({ event: action.DELETE_MODEL, id: target }); } hash_model(id) { this.post({ event: action.HASH_MODEL, id: id || this.model.id }); } list_models() { this.post({ event: action.LIST_MODELS }); } // mode "chat" runs a full message list through the chat template; // mode "completion" sends the prompt text as-is run({ mode = "completion", prompt = "", messages = null, stop = [], chat = false, seed = -1, max_token_len = 64, top_k = 40, top_p = 0.9, temp = 0.7, repeat_last_n = 32, repeat_penalty = 1.1, context_size = 0, chat_template = "", chat_template_source = "", tools = "[]", template_kwargs = "{}", grammar = "", json_schema = "", regex = "", } = {}) { const msg = { event: action.RUN_MAIN, mode, prompt, messages, stop, chat, seed, max_token_len, top_k, top_p, temp, repeat_last_n, repeat_penalty, context_size, chat_template, chat_template_source, tools, template_kwargs, grammar, json_schema, regex, }; // allowed before the model finished loading, like the original docs suggest if (!this.model_loaded) { this._pending = msg; return; } this.post(msg); } _flushPending() { if (!this._pending) return; const msg = this._pending; this._pending = null; this.post(msg); } terminate() { if (this.worker) this.worker.terminate(); this.worker = null; this.model_loaded = false; } } export { LLM };