Spaces:
Running
Running
Download worker.js from lemonteaa/llama-cpp-wasm: direct link, hf CLI and curl.
- Browser
- Download file 25.9 kB
-
https://huggingface.co/spaces/lemonteaa/llama-cpp-wasm/resolve/main/worker.js
- Command line
-
hf download hf://spaces/lemonteaa/llama-cpp-wasm/worker.js
-
curl -L -o worker.js https://huggingface.co/spaces/lemonteaa/llama-cpp-wasm/resolve/main/worker.js
25.9 kB
| // Inference worker. | |
| // | |
| // Responsibilities: | |
| // 1. add models - two ingest paths into one library: | |
| // url -> resumable, pausable, chunked download | |
| // file -> the user's own File, read in 4 MiB slices, never copied | |
| // 2. sha256 each model once, so a human can compare it with the publisher's | |
| // 3. stream the bytes into the Emscripten FS (from IndexedDB chunks, or | |
| // straight off the local File) | |
| // 4. load it with the C API and stream generated tokens back | |
| // | |
| // Both ingest paths end in the same kind of record, so loading, hashing, | |
| // renaming and deleting are one code path (see storage.js for the schema). | |
| // The module is imported lazily: a pthread build needs SharedArrayBuffer, which | |
| // exists only on a cross-origin isolated page (COOP/COEP). Pages without it get | |
| // the single-thread module, so the demo still runs on plain static hosting. | |
| const THREADED = typeof SharedArrayBuffer === "function" && self.crossOriginIsolated === true; | |
| const HARDWARE_THREADS = (typeof navigator !== "undefined" && navigator.hardwareConcurrency) || 1; | |
| import { action } from "./actions.js"; | |
| import { downloadModel } from "./download.js"; | |
| import { sha256Chunks } from "./sha256.js"; | |
| import { | |
| CHUNK_SIZE, getModel, putModel, updateModel, listModels, newModelId, nameFromUrl, | |
| putChunk, iterateChunks, clearChunks, deleteModel, findModelByUrl, | |
| } from "./storage.js"; | |
| let module = null; | |
| let loaded = false; | |
| let loadedId = null; | |
| let currentNctx = 1024; | |
| // Engine launch parameters, applied when a context is created: the KV cache | |
| // configuration is fixed for the life of a llama_context, so changing it means | |
| // reloading the model (the page does that through Reload engine). | |
| let currentKv = { type_k: "f16", type_v: "f16", flash_attn: -1, kv_unified: true, n_parallel: 1 }; | |
| let currentThreads = 1; // 1 unless the page asked for and can run more | |
| const CTX_ARGS = ["number", "string", "string", "number", "number", "number"]; | |
| function createContext(n_ctx) { | |
| const n = module.ccall("llmjs_create_context_ex", "number", CTX_ARGS, | |
| [n_ctx, currentKv.type_k, currentKv.type_v, currentKv.flash_attn, | |
| currentKv.kv_unified ? 1 : 0, currentKv.n_parallel || 1]); | |
| if (n >= 0) currentNctx = n; | |
| return n; | |
| } | |
| function ctxConfig() { | |
| const text = module.UTF8ToString(module._llmjs_ctx_config()); | |
| try { return JSON.parse(text); } catch (_) { return null; } | |
| } | |
| // Files the page handed over in this session, by model id. A "file" model with | |
| // no copy in IndexedDB needs its File again after a reload - see start(). | |
| const files = new Map(); | |
| let abort = null; // AbortController for the active download | |
| let busy = false; | |
| let lastRequest = null; // { id, context_size } so Resume can continue | |
| const post = (msg) => self.postMessage(msg); | |
| const progress = (p) => post({ event: action.PROGRESS, ...p }); | |
| const log = (line, level = 3) => post({ event: action.LOG, level, line }); | |
| const fail = (err) => post({ event: action.ERROR, error: String((err && err.message) || err) }); | |
| // --- wasm -> JS callbacks --------------------------------------------------- | |
| self.llmjs_on_token = (text) => post({ event: action.WRITE_RESULT, line: text }); | |
| self.llmjs_on_log = (level, text) => post({ event: action.LOG, level, line: text }); | |
| const tick = () => new Promise((r) => setTimeout(r, 0)); | |
| // storage.js shaped for download.js | |
| const storage = { | |
| CHUNK_SIZE, getMeta: getModel, putMeta: putModel, updateMeta: updateModel, putChunk, clearChunks, | |
| }; | |
| // Render a message list with llama.cpp's chat stack (GGUF template, tools and | |
| // chat_template_kwargs), inside the wasm module. Throws with the engine's | |
| // message; the caller decides whether that is worth showing. | |
| function renderTemplateEx(messagesJson, tools, source, kwargs) { | |
| const sig = ["string", "string", "string", "string", "number", "number", "number"]; | |
| const need = module.ccall("llmjs_chat_apply_ex", "number", sig, | |
| [messagesJson, tools, source, kwargs, 1, 0, 0]); | |
| if (need < 0) { | |
| throw new Error(module.UTF8ToString(module._llmjs_last_error())); | |
| } | |
| const size = need + 1; | |
| const ptr = module._malloc(size); | |
| try { | |
| module.ccall("llmjs_chat_apply_ex", "number", sig, | |
| [messagesJson, tools, source, kwargs, 1, ptr, size]); | |
| return module.UTF8ToString(ptr, need); | |
| } finally { | |
| module._free(ptr); | |
| } | |
| } | |
| let modulePromise = null; | |
| async function ensureModule() { | |
| if (module) return module; | |
| if (!modulePromise) { | |
| modulePromise = (async () => { | |
| // No locateFile override: the glue resolves its .wasm relative to its | |
| // own import.meta.url (web/wasm/). Passing one here resolves against | |
| // *this* module's URL and 404s the .wasm into an HTML page. | |
| let factory; | |
| if (THREADED) { | |
| try { | |
| factory = (await import("./wasm/llm-wasm-mt.js")).default; | |
| } catch (err) { | |
| log(`threaded build not found (${err}) - using the single-thread module`); | |
| } | |
| } | |
| if (!factory) factory = (await import("./wasm/llm-wasm.js")).default; | |
| // emscripten writes to stdout/stderr for anything that does not go | |
| // through our log callback; that is chatter, not a failure, so it | |
| // must not land in the console as an error | |
| const print = (text) => self.llmjs_on_log(2, String(text).replace(/\n?$/, "\n")); | |
| module = await factory({ print, printErr: print }); | |
| module._llmjs_init(); | |
| log(`llama.cpp ${module.UTF8ToString(module._llmjs_version())}` + | |
| (THREADED ? " (threaded build)" : " (single-thread build)")); | |
| return module; | |
| })(); | |
| } | |
| return modulePromise; | |
| } | |
| // --- byte sources ----------------------------------------------------------- | |
| // 4 MiB slices of a File the user picked. The bytes stay on disk; each slice is | |
| // read, handed to the FS (or the hash), and dropped again. | |
| async function* fileSlices(file) { | |
| for (let offset = 0; offset < file.size; offset += CHUNK_SIZE) { | |
| const end = Math.min(offset + CHUNK_SIZE, file.size); | |
| yield new Uint8Array(await file.slice(offset, end).arrayBuffer()); | |
| } | |
| } | |
| // Single iterator over a model's bytes, wherever they live. | |
| async function* byteStream(model) { | |
| if (model.source === "url" || model.copied) { | |
| for await (const row of iterateChunks(model.id)) yield row.data; | |
| return; | |
| } | |
| const file = files.get(model.id); | |
| if (!file) throw new Error("the local file is not attached - choose it again"); | |
| yield* fileSlices(file); | |
| } | |
| async function hashModel(model) { | |
| const total = model.total || 0; | |
| const sha256 = await sha256Chunks(byteStream(model), (done) => | |
| progress({ phase: "hash", loaded: done, total })); | |
| return updateModel(model.id, { sha256 }); | |
| } | |
| // --- ingest ---------------------------------------------------------------- | |
| async function addUrlModel({ url, name, expectedSha256, mmproj }) { | |
| let model = await findModelByUrl(url); | |
| if (model) { | |
| // same URL added again: treat it as "open", but let the name/hash be corrected | |
| const patch = {}; | |
| if (name) patch.name = name; | |
| if (expectedSha256) patch.expectedSha256 = expectedSha256; | |
| if (mmproj !== undefined) patch.multimodal = !!mmproj; | |
| if (Object.keys(patch).length) model = await updateModel(model.id, patch); | |
| } else { | |
| model = await putModel({ | |
| id: newModelId(), | |
| name: (name || "").trim() || nameFromUrl(url), | |
| source: "url", | |
| url, | |
| fileName: null, | |
| total: 0, | |
| received: 0, | |
| complete: false, | |
| copied: true, // url models always live in the browser | |
| sha256: null, | |
| expectedSha256: (expectedSha256 || "").trim() || null, | |
| // user-flagged: this GGUF is a multimodal projector, not a text model | |
| multimodal: !!mmproj, | |
| created: Date.now(), | |
| }); | |
| } | |
| post({ event: action.MODEL_ADDED, model }); | |
| return model.id; | |
| } | |
| async function addFileModel({ file, name, keepCopy, expectedSha256, mmproj }) { | |
| if (!file) throw new Error("no file given"); | |
| const head = new Uint8Array(await file.slice(0, 4).arrayBuffer()); | |
| const magic = String.fromCharCode(...head); | |
| if (magic !== "GGUF") { | |
| throw new Error(`${file.name} is not a GGUF file (starts with "${magic}")`); | |
| } | |
| const model = await putModel({ | |
| id: newModelId(), | |
| name: (name || "").trim() || file.name.replace(/\.gguf$/i, ""), | |
| source: "file", | |
| url: null, | |
| fileName: file.name, | |
| total: file.size, | |
| received: keepCopy ? 0 : file.size, | |
| complete: !keepCopy, | |
| copied: false, // filled in by copyFileIntoStorage below | |
| sha256: null, | |
| expectedSha256: (expectedSha256 || "").trim() || null, | |
| multimodal: !!mmproj, | |
| created: Date.now(), | |
| }); | |
| files.set(model.id, file); | |
| post({ event: action.MODEL_ADDED, model }); | |
| if (keepCopy) return (await copyFileIntoStorage(model, file)).id; | |
| return model.id; | |
| } | |
| // opt-in: put the bytes in IndexedDB so the model no longer needs the file | |
| async function copyFileIntoStorage(model, file) { | |
| await clearChunks(model.id); | |
| let index = 0; | |
| let received = 0; | |
| for await (const bytes of fileSlices(file)) { | |
| const copy = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); | |
| await putChunk(model.id, index++, copy); | |
| received += bytes.length; | |
| progress({ phase: "copy", loaded: received, total: file.size }); | |
| } | |
| return updateModel(model.id, { copied: true, received, total: file.size, complete: true }); | |
| } | |
| // --- loading --------------------------------------------------------------- | |
| async function loadIntoFS(model, target = "/models/model.gguf") { | |
| try { module.FS.mkdir("/models"); } catch (_) { /* already exists */ } | |
| try { module.FS.unlink(target); } catch (_) { /* not there */ } | |
| const stream = module.FS.open(target, "w+"); | |
| let written = 0; | |
| try { | |
| for await (const chunk of byteStream(model)) { | |
| const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk); | |
| module.FS.write(stream, bytes, 0, bytes.length, written); | |
| written += bytes.length; | |
| progress({ phase: "load", loaded: written, total: model.total || written }); | |
| await tick(); | |
| } | |
| } finally { | |
| module.FS.close(stream); | |
| } | |
| return written; | |
| } | |
| // `ingest_only` stops after the bytes are in the library (download + hash): a | |
| // projector is not something the engine can "start", it is picked as a launch | |
| // parameter of the next model load. | |
| async function start({ id, file, mmproj_file, context_size, kv, mmproj_id, threads, | |
| ingest_only = false }) { | |
| if (busy) { | |
| fail(new Error("a load is already running")); | |
| return; | |
| } | |
| if (file) files.set(id, file); | |
| if (mmproj_id && mmproj_file) files.set(mmproj_id, mmproj_file); | |
| if (kv) currentKv = { ...currentKv, ...kv }; | |
| if (threads > 1 && !THREADED) { | |
| log("no threaded wasm on this page (needs COOP/COEP + SharedArrayBuffer) - using 1 thread"); | |
| } | |
| currentThreads = THREADED ? Math.max(1, Math.min(64, threads || 1)) : 1; | |
| busy = true; | |
| abort = new AbortController(); | |
| lastRequest = { id, context_size, kv: { ...currentKv }, mmproj_id, threads: currentThreads, | |
| ingest_only }; | |
| try { | |
| let model = await getModel(id); | |
| if (!model) throw new Error(`unknown model ${id}`); | |
| // 1. get the bytes into the browser (url models only; a local file is | |
| // already where it needs to be) | |
| if (model.source === "url" && !model.complete) { | |
| await downloadModel( | |
| { id: model.id, url: model.url, total: model.total }, | |
| { signal: abort.signal, onProgress: progress, storage }, | |
| ); | |
| model = await getModel(id); | |
| } | |
| if (model.source === "file" && !model.copied && !files.has(model.id)) { | |
| throw new Error("the local file is not attached - choose it again"); | |
| } | |
| // 2. identify the bytes once | |
| if (!model.sha256) model = await hashModel(model); | |
| if (ingest_only) { | |
| busy = false; | |
| post({ event: action.MODEL_UPDATED, model }); | |
| return; | |
| } | |
| // 3. into the wasm FS, then load | |
| await ensureModule(); | |
| // older single-thread modules predate this export; they clamp to 1 anyway | |
| if (module._llmjs_set_threads) { | |
| module.ccall("llmjs_set_threads", null, ["number"], [currentThreads]); | |
| } | |
| // the wasm instance can only hold one model at a time | |
| if (loaded && loadedId !== id) { | |
| module._llmjs_free(); | |
| loaded = false; | |
| loadedId = null; | |
| } | |
| const bytes = await loadIntoFS(model); | |
| if (model.received && bytes !== model.received) { | |
| log(`stored model size ${bytes} != expected ${model.received}`); | |
| } | |
| currentNctx = context_size || currentNctx; | |
| const n = module.ccall("llmjs_load_model", "number", ["string", "number"], | |
| ["/models/model.gguf", currentNctx]); | |
| if (n < 0) throw new Error(`llmjs_load_model failed (${n})`); | |
| // load_model builds a context with the defaults; this applies the KV | |
| // options the page asked for (a no-op when they already match) | |
| if (createContext(currentNctx) < 0) { | |
| throw new Error("could not create a context with the requested KV cache settings"); | |
| } | |
| // optional multimodal projector: a second GGUF from the library, loaded | |
| // by llama.cpp's mtmd stack (llama-server --mmproj) | |
| let mmproj = null; | |
| if (mmproj_id) { | |
| mmproj = await getModel(mmproj_id); | |
| if (!mmproj) throw new Error(`unknown mmproj ${mmproj_id}`); | |
| if (mmproj.source === "file" && !mmproj.copied && !files.has(mmproj.id)) { | |
| throw new Error("the mmproj file is not attached - choose it again"); | |
| } | |
| await loadIntoFS(mmproj, "/models/mmproj.gguf"); | |
| const rc = module.ccall("llmjs_mmproj_load", "number", ["string"], | |
| ["/models/mmproj.gguf"]); | |
| if (rc <= 0) throw new Error("could not load the multimodal projector"); | |
| } else { | |
| module.ccall("llmjs_mmproj_free", null, [], []); | |
| } | |
| loaded = true; | |
| loadedId = id; | |
| busy = false; | |
| post({ | |
| event: action.INITIALIZED, | |
| model, | |
| bytes, | |
| n_ctx: module.ccall("llmjs_n_ctx", "number", [], []), | |
| ctx_config: ctxConfig(), | |
| threads: currentThreads, | |
| threaded_build: THREADED, | |
| hardware_threads: HARDWARE_THREADS, | |
| mmproj: (() => { | |
| const text = module.UTF8ToString(module._llmjs_mmproj_info()); | |
| try { return JSON.parse(text); } catch (_) { return null; } | |
| })(), | |
| mmproj_name: mmproj ? mmproj.name : null, | |
| chat_template_name: module.UTF8ToString(module._llmjs_chat_template_name()), | |
| // the GGUF's own Jinja template, rendered by the engine inside the wasm build | |
| chat_template: module.UTF8ToString(module._llmjs_chat_template()), | |
| has_chat_template: module._llmjs_has_chat_template() !== 0, | |
| }); | |
| post({ event: action.MODEL_UPDATED, model }); | |
| } catch (err) { | |
| busy = false; | |
| if (err && err.name === "AbortError") { | |
| post({ event: action.PAUSED, id }); | |
| } else { | |
| fail(err); | |
| } | |
| } | |
| } | |
| // Set by a STOP message. The generation loop below checks it between tokens, | |
| // which is only possible because generation is stepped from JS instead of one | |
| // long blocking call: inside a single ccall this worker could not read messages. | |
| let cancelRequested = false; | |
| let generating = false; | |
| // mode "chat": a message list rendered through the chat template. | |
| // mode "completion": the prompt is the prompt, no template involved. | |
| // Both honour the same sampling limits and stop sequences. | |
| async function run(opts) { | |
| const { | |
| mode = "completion", | |
| prompt = "", | |
| messages = null, | |
| stop = [], | |
| 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 = "", | |
| images = [], | |
| } = opts; | |
| if (!loaded || !module) { | |
| fail(new Error("no model loaded")); | |
| return; | |
| } | |
| if (generating) { | |
| fail(new Error("a run is already in progress")); | |
| return; | |
| } | |
| // structured output: only when the page chose a schema. The tool-call path | |
| // sends neither, so it runs exactly as before. `grammar` above is a const | |
| // destructured binding, so the schema's result needs its own local. | |
| let grammarText = grammar; | |
| if (!grammarText && json_schema && json_schema.trim()) { | |
| if (typeof module._llmjs_grammar_from_schema !== "function") { | |
| fail(new Error("this module build has no JSON schema support; the threaded build " | |
| + "(needs COOP/COEP) has it, or paste a GBNF grammar instead")); | |
| return; | |
| } | |
| const asGrammar = module.ccall("llmjs_grammar_from_schema", "string", | |
| ["string"], [json_schema]); | |
| if (!asGrammar) { | |
| const why = module.UTF8ToString(module._llmjs_last_error()); | |
| fail(new Error(`could not use the JSON schema: ${why}`)); | |
| return; | |
| } | |
| grammarText = asGrammar; | |
| } | |
| if (createContext(context_size || currentNctx) < 0) { | |
| fail(new Error("could not create the inference context")); | |
| return; | |
| } | |
| // stop sequences are matched in C++ against the decoded text, so a stop | |
| // string is never streamed to the page | |
| module.ccall("llmjs_set_stop_sequences", null, ["string"], | |
| [JSON.stringify(Array.isArray(stop) ? stop : [])]); | |
| // data URLs for the images of this run; the chat template has one media | |
| // marker per image in the messages it renders | |
| module.ccall("llmjs_set_images", null, ["string"], | |
| [JSON.stringify(Array.isArray(images) ? images : [])]); | |
| // arm the engine: everything promised by the parameters goes in here | |
| let rc; | |
| if (mode === "chat") { | |
| const list = Array.isArray(messages) && messages.length | |
| ? messages | |
| : [{ role: "user", content: prompt }]; | |
| rc = module.ccall( | |
| "llmjs_gen_begin_chat", | |
| "number", | |
| ["string", "string", "string", "string", "number", "number", "number", | |
| "number", "number", "number", "number", "string"], | |
| [JSON.stringify(list), tools || "[]", chat_template_source, template_kwargs || "{}", | |
| max_token_len, temp, top_k, top_p, repeat_penalty, repeat_last_n, seed, grammarText], | |
| ); | |
| } else { | |
| rc = module.ccall( | |
| "llmjs_gen_begin_raw", | |
| "number", | |
| ["string", "number", "number", "number", "number", "number", | |
| "number", "number", "string"], | |
| [prompt, max_token_len, temp, top_k, top_p, repeat_penalty, | |
| repeat_last_n, seed, grammarText], | |
| ); | |
| } | |
| if (rc < 0) { | |
| // the engine knows why: an image larger than the context, a marker/image | |
| // mismatch, no projector … - "(-1)" alone tells the user nothing | |
| const why = module.UTF8ToString(module._llmjs_last_error()); | |
| fail(new Error(`could not start generation (${rc})${why ? `: ${why}` : ""}`)); | |
| return; | |
| } | |
| generating = true; | |
| cancelRequested = false; | |
| const t0 = performance.now(); | |
| let tokens = 0; | |
| try { | |
| // one token per turn of the event loop: a STOP message is handled | |
| // during the await, and the loop then exits with the text so far | |
| while (!cancelRequested) { | |
| if (module.ccall("llmjs_gen_step", "number", [], []) <= 0) break; | |
| await tick(); | |
| } | |
| tokens = module.ccall("llmjs_gen_end", "number", [], []); | |
| } finally { | |
| generating = false; | |
| } | |
| const ms = performance.now() - t0; | |
| const interrupted = cancelRequested; | |
| cancelRequested = false; | |
| post({ | |
| event: action.RUN_COMPLETED, | |
| mode, | |
| tokens, | |
| ms, | |
| interrupted, | |
| tokens_per_second: tokens > 0 ? (tokens * 1000) / ms : 0, | |
| // the final text, with any stop sequence removed: whatever the page | |
| // streamed may still contain the beginning of a stop string | |
| output: module.UTF8ToString(module._llmjs_last_output()), | |
| // prompt_tokens / reused_tokens / prompt_ms of this run | |
| run_stats: (() => { | |
| const text = module.UTF8ToString(module._llmjs_last_run_stats()); | |
| if (!text) return null; | |
| try { return JSON.parse(text); } catch (_) { return null; } | |
| })(), | |
| // jinja / jinja:override / builtin:<name> / raw | |
| template_strategy: module.UTF8ToString(module._llmjs_chat_template_strategy()), | |
| // { content, reasoning_content, tool_calls } when the template's parser | |
| // recognised the reply, null when it did not | |
| parsed: (() => { | |
| const text = module.UTF8ToString(module._llmjs_parsed_output()); | |
| if (!text) return null; | |
| try { return JSON.parse(text); } catch (_) { return null; } | |
| })(), | |
| }); | |
| } | |
| self.addEventListener("message", (e) => { | |
| const msg = e.data; | |
| switch (msg.event) { | |
| case action.ADD_URL: | |
| (async () => { | |
| const id = await addUrlModel(msg); | |
| // a projector is not a model to run: download its bytes into the | |
| // library, then stop - it is picked in the engine parameters and | |
| // loaded next to whatever model is selected | |
| await start({ id, context_size: msg.context_size, kv: msg.kv, threads: msg.threads, | |
| ingest_only: !!msg.mmproj }); | |
| })().catch(fail); | |
| break; | |
| case action.ADD_FILE: | |
| (async () => { | |
| const id = await addFileModel(msg); | |
| if (msg.mmproj) return; | |
| await start({ id, context_size: msg.context_size, kv: msg.kv, threads: msg.threads }); | |
| })().catch(fail); | |
| break; | |
| case action.LOAD: | |
| start(msg); | |
| break; | |
| case action.PAUSE: | |
| if (abort) abort.abort(); | |
| break; | |
| case action.RESUME: | |
| if (lastRequest) start(lastRequest); | |
| else fail(new Error("nothing to resume")); | |
| break; | |
| case action.PREVIEW_TEMPLATE: | |
| (async () => { | |
| await ensureModule(); | |
| const messages = JSON.stringify(msg.messages && msg.messages.length | |
| ? msg.messages | |
| : [{ role: "user", content: msg.prompt || "" }]); | |
| const text = renderTemplateEx(messages, msg.tools || "[]", | |
| msg.template_source || "", msg.template_kwargs || "{}"); | |
| post({ event: action.TEMPLATE_PREVIEW, text }); | |
| })().catch(fail); | |
| break; | |
| case action.RELOAD_MODEL: | |
| (async () => { | |
| // a real reload: drop the model and the backend, then load again | |
| if (module) { | |
| module._llmjs_free(); | |
| module._llmjs_init(); | |
| } | |
| loaded = false; | |
| loadedId = null; | |
| await start({ | |
| id: msg.id, | |
| file: files.get(msg.id) || null, | |
| context_size: msg.context_size, | |
| kv: msg.kv, | |
| threads: msg.threads, | |
| mmproj_id: msg.mmproj_id, | |
| mmproj_file: msg.mmproj_id ? files.get(msg.mmproj_id) || null : null, | |
| }); | |
| })().catch(fail); | |
| break; | |
| case action.HASH_MODEL: | |
| (async () => { | |
| const model = await getModel(msg.id); | |
| if (!model) throw new Error(`unknown model ${msg.id}`); | |
| const hashed = await hashModel(model); | |
| post({ event: action.MODEL_UPDATED, model: hashed }); | |
| })().catch(fail); | |
| break; | |
| case action.DELETE_MODEL: | |
| (async () => { | |
| if (loaded && loadedId === msg.id) { | |
| module._llmjs_free(); | |
| loaded = false; | |
| loadedId = null; | |
| } | |
| files.delete(msg.id); | |
| const chunks = await deleteModel(msg.id); | |
| post({ event: action.DELETED, id: msg.id, chunks }); | |
| })().catch(fail); | |
| break; | |
| case action.LIST_MODELS: | |
| listModels() | |
| .then((models) => post({ event: action.MODELS, models })) | |
| .catch(fail); | |
| break; | |
| case action.STOP: | |
| if (generating) cancelRequested = true; | |
| break; | |
| case action.RUN_MAIN: | |
| run(msg).catch(fail); | |
| break; | |
| } | |
| }, false); | |