Spaces:
Running
Running
lemonteaa commited on
Commit ·
685ad0d
1
Parent(s): f375a7a
update dist bundle (2)
Browse files- index.html +271 -63
- llm.js +3 -1
- wasm/llm-wasm-mt.build.txt +1 -1
- wasm/llm-wasm-mt.js +0 -0
- wasm/llm-wasm-mt.wasm +2 -2
- worker.js +53 -21
index.html
CHANGED
|
@@ -36,6 +36,21 @@
|
|
| 36 |
button:disabled { opacity: .45; cursor: default; }
|
| 37 |
#bar { height: 8px; background: #0d0f12; border: 1px solid #2a2f37; border-radius: 999px; overflow: hidden; margin: 10px 0 8px; }
|
| 38 |
#barFill { height: 100%; width: 0%; background: linear-gradient(90deg, #2f6fed, #5aa0ff); transition: width .15s linear; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
#status { color: #9aa1ab; font-size: 13px; min-height: 20px; }
|
| 40 |
#out { white-space: pre-wrap; word-break: break-word; background: #0d0f12; border: 1px solid #2a2f37;
|
| 41 |
border-radius: 6px; padding: 12px; min-height: 140px; }
|
|
@@ -44,16 +59,39 @@
|
|
| 44 |
summary { cursor: pointer; color: #9aa1ab; font-size: 13px; }
|
| 45 |
#log { max-height: 180px; overflow: auto; background: #0d0f12; border: 1px solid #2a2f37; border-radius: 6px;
|
| 46 |
padding: 8px 10px; margin-top: 8px; font-size: 12px; color: #8a8f98; white-space: pre-wrap; }
|
|
|
|
|
|
|
|
|
|
| 47 |
.pill { display: inline-block; padding: 1px 7px; border-radius: 999px; background: #23303f; color: #9cc3ff;
|
| 48 |
font-size: 11px; margin-left: 6px; }
|
| 49 |
|
| 50 |
/* --- top bar ---------------------------------------------------------- */
|
| 51 |
.appbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; }
|
| 52 |
.appbar label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #8a8f98; }
|
| 53 |
-
|
|
|
|
|
|
|
| 54 |
.appbar button { padding: 7px 12px; font-size: 13px; }
|
| 55 |
#currentModel { flex: 1 1 100%; font-size: 12px; }
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
/* --- tabs ------------------------------------------------------------- */
|
| 58 |
.tabs { display: flex; flex-wrap: wrap; gap: 4px; margin: 18px 0 0; border-bottom: 1px solid #2a2f37; }
|
| 59 |
.tab { background: transparent; color: #9aa1ab; border: 1px solid transparent; border-bottom: 0;
|
|
@@ -186,6 +224,43 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 186 |
<button id="stop" class="danger" disabled title="stop generating, keeping what has arrived">Stop</button>
|
| 187 |
<div id="currentModel" class="muted"></div>
|
| 188 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
<div id="bar"><div id="barFill"></div></div>
|
| 190 |
<div id="status">idle</div>
|
| 191 |
|
|
@@ -239,38 +314,6 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 239 |
</div>
|
| 240 |
|
| 241 |
<aside class="params" id="paramsRail">
|
| 242 |
-
<h2>engine <span class="pill" title="fixed when the context is created">launch</span></h2>
|
| 243 |
-
<label class="field">context length <input type="number" id="ctxSize" value="1024" step="256"
|
| 244 |
-
min="128" max="16384" title="tokens; KV memory grows with this"></label>
|
| 245 |
-
<label class="field">KV cache K <select id="kvTypeK" aria-label="KV cache type for K">
|
| 246 |
-
<option value="f16" selected>f16</option>
|
| 247 |
-
<option value="q8_0">q8_0</option>
|
| 248 |
-
<option value="q4_0">q4_0</option>
|
| 249 |
-
</select></label>
|
| 250 |
-
<label class="field">KV cache V <select id="kvTypeV" aria-label="KV cache type for V">
|
| 251 |
-
<option value="f16" selected>f16</option>
|
| 252 |
-
<option value="q8_0">q8_0</option>
|
| 253 |
-
<option value="q4_0">q4_0</option>
|
| 254 |
-
</select></label>
|
| 255 |
-
<label class="field">flash attention <select id="flashAttn" aria-label="flash attention">
|
| 256 |
-
<option value="-1" selected>auto</option>
|
| 257 |
-
<option value="0">off</option>
|
| 258 |
-
<option value="1">on</option>
|
| 259 |
-
</select></label>
|
| 260 |
-
<label class="field">threads <input type="number" id="threads" min="1" max="16" step="1" value="1"
|
| 261 |
-
aria-label="CPU threads"></label>
|
| 262 |
-
<p class="hint" id="threadsHint" style="margin-top:-4px"></p>
|
| 263 |
-
<label class="row"><input type="checkbox" id="kvUnified" checked> unified KV cache</label>
|
| 264 |
-
<label class="row"><input type="checkbox" id="useMmproj"> load multimodal projector</label>
|
| 265 |
-
<label class="field">mmproj <select id="mmprojSelect" aria-label="multimodal projector" disabled></select></label>
|
| 266 |
-
<p class="hint" style="margin-top:6px">Flag a GGUF as <b>mmproj</b> in the model library to
|
| 267 |
-
choose it here. Then attach images to a message (button or drag & drop); the image goes
|
| 268 |
-
into the prompt as the projector's media marker.</p>
|
| 269 |
-
<p class="hint" style="margin-top:6px">Applied when a model loads \u2014 press
|
| 270 |
-
<b>Reload engine</b> after changing them. One parallel sequence: extra slots would only
|
| 271 |
-
duplicate a cache a single user cannot fill. A quantized V cache needs flash attention;
|
| 272 |
-
asking for one switches it on. KV memory grows with context length.</p>
|
| 273 |
-
|
| 274 |
<h2>sampling</h2>
|
| 275 |
<label class="field">temperature <input type="number" id="temp" value="0.7" step="0.05" min="0" max="2"></label>
|
| 276 |
<label class="field">top_p <input type="number" id="topP" value="0.9" step="0.05" min="0" max="1"></label>
|
|
@@ -284,17 +327,38 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 284 |
<label class="field wide">stop sequences
|
| 285 |
<textarea id="stopSequences" spellcheck="false" placeholder="one per line (\n and \t are expanded)"></textarea>
|
| 286 |
</label>
|
| 287 |
-
<h2>
|
| 288 |
-
<label class="field wide">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
<textarea id="tools" spellcheck="false" placeholder='[{"type":"function","function":{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}}}}}]'></textarea>
|
| 290 |
</label>
|
| 291 |
-
<
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
<label class="row"><input type="checkbox" id="enableThinking"> enable_thinking</label>
|
| 296 |
-
<p class="hint" style="margin-top:6px">
|
| 297 |
-
|
|
|
|
|
|
|
| 298 |
</aside>
|
| 299 |
</div>
|
| 300 |
|
|
@@ -331,6 +395,11 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 331 |
<section class="panel" id="panel-models" hidden>
|
| 332 |
<h2>model library</h2>
|
| 333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
<div class="add">
|
| 335 |
<h3>from a URL</h3>
|
| 336 |
<div class="addrow">
|
|
@@ -394,16 +463,53 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 394 |
const pct = (loaded, total) => (total ? Math.min(100, (loaded / total) * 100) : 0);
|
| 395 |
|
| 396 |
const statusText = (t) => { $("status").textContent = t; };
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
| 398 |
const el = $("log");
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
el.scrollTop = el.scrollHeight;
|
| 401 |
};
|
| 402 |
const setBar = (loaded, total) => { $("barFill").style.width = pct(loaded, total).toFixed(1) + "%"; };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
|
| 404 |
let app = null;
|
| 405 |
let models = [];
|
| 406 |
let loadedId = null; // model currently in the wasm instance
|
|
|
|
| 407 |
let currentId = null; // model the user last asked to load
|
| 408 |
let pendingFile = null; // file picked for the "add" form
|
| 409 |
let reattachFor = null; // model id waiting for its file to be re-picked
|
|
@@ -517,6 +623,10 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 517 |
stop: stopList(),
|
| 518 |
chat_template_source: templateOverride(),
|
| 519 |
tools: $("tools").value.trim(),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
template_kwargs: $("templateKwargs").value.trim() || "{}",
|
| 521 |
};
|
| 522 |
}
|
|
@@ -553,7 +663,9 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 553 |
parent.postMessage({ type: "llmjs:ready", model: m.name,
|
| 554 |
n_ctx: info.n_ctx, threads: info.threads || 1 }, "*");
|
| 555 |
}
|
| 556 |
-
|
|
|
|
|
|
|
| 557 |
setState("ready");
|
| 558 |
refreshModels();
|
| 559 |
},
|
|
@@ -631,31 +743,53 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 631 |
chatRow.querySelector(".content").readOnly = false;
|
| 632 |
chatRow = null;
|
| 633 |
}
|
| 634 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
log("ERROR: " + err);
|
| 636 |
setState(loadedId ? "ready" : "idle");
|
| 637 |
},
|
| 638 |
(p) => {
|
|
|
|
| 639 |
if (p.phase === "download") {
|
|
|
|
| 640 |
setState("downloading");
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
} else if (p.phase === "copy") {
|
| 645 |
setState("copying");
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
| 648 |
} else if (p.phase === "hash") {
|
| 649 |
setState("hashing");
|
| 650 |
-
|
| 651 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
} else if (p.phase === "load") {
|
|
|
|
| 653 |
setState("loading");
|
| 654 |
-
statusText(`loading into wasm ${fmt(p.loaded)} / ${fmt(p.total)}`);
|
| 655 |
setBar(p.loaded, p.total);
|
| 656 |
}
|
| 657 |
},
|
| 658 |
-
(line) => log(line.trimEnd()),
|
| 659 |
);
|
| 660 |
|
| 661 |
app.on_event = (msg) => {
|
|
@@ -667,17 +801,27 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 667 |
$("fileLabel").value = "";
|
| 668 |
$("urlMmproj").checked = false;
|
| 669 |
$("fileMmproj").checked = false;
|
|
|
|
| 670 |
// adding a projector does not load anything, so nothing reports INITIALIZED
|
| 671 |
setState(loadedId ? "ready" : "idle");
|
| 672 |
refreshModels();
|
| 673 |
break;
|
| 674 |
|
| 675 |
case action.MODEL_UPDATED:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 676 |
refreshModels();
|
| 677 |
break;
|
| 678 |
|
| 679 |
case action.PAUSED:
|
| 680 |
-
|
|
|
|
| 681 |
setState("paused");
|
| 682 |
refreshModels();
|
| 683 |
break;
|
|
@@ -1039,7 +1183,7 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 1039 |
|
| 1040 |
function startLoad(m) {
|
| 1041 |
if (m.multimodal) {
|
| 1042 |
-
statusText(`${m.name} is an mmproj — tick
|
| 1043 |
return;
|
| 1044 |
}
|
| 1045 |
// a local file that was never copied needs its File again after a reload
|
|
@@ -1062,7 +1206,17 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 1062 |
currentId = m.id;
|
| 1063 |
$("out").textContent = "";
|
| 1064 |
$("stats").textContent = "";
|
| 1065 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1066 |
setState("starting");
|
| 1067 |
app.post({
|
| 1068 |
event: action.LOAD,
|
|
@@ -1082,6 +1236,8 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 1082 |
if (!app || !currentId) return;
|
| 1083 |
$("out").textContent = "";
|
| 1084 |
$("stats").textContent = "";
|
|
|
|
|
|
|
| 1085 |
statusText("freeing the model and reloading the engine…");
|
| 1086 |
setState("starting");
|
| 1087 |
const mpId = engineParams().mmproj_id;
|
|
@@ -1118,7 +1274,19 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 1118 |
ensureApp();
|
| 1119 |
$("out").textContent = "";
|
| 1120 |
$("stats").textContent = "";
|
| 1121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1122 |
setState("starting");
|
| 1123 |
app.post({
|
| 1124 |
event: action.ADD_FILE,
|
|
@@ -1137,7 +1305,15 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 1137 |
ensureApp();
|
| 1138 |
$("out").textContent = "";
|
| 1139 |
$("stats").textContent = "";
|
| 1140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1141 |
setState("starting");
|
| 1142 |
app.post({
|
| 1143 |
event: action.ADD_URL,
|
|
@@ -1281,12 +1457,13 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 1281 |
}
|
| 1282 |
|
| 1283 |
$("pause").onclick = () => {
|
| 1284 |
-
if (app) { app.pause(); statusText("pausing…"); }
|
| 1285 |
};
|
| 1286 |
|
| 1287 |
$("resume").onclick = () => {
|
| 1288 |
if (!app) return;
|
| 1289 |
-
|
|
|
|
| 1290 |
setState("downloading");
|
| 1291 |
app.resume();
|
| 1292 |
};
|
|
@@ -1316,6 +1493,16 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 1316 |
syncTemplateUI();
|
| 1317 |
};
|
| 1318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1319 |
$("previewTemplate").onclick = () => {
|
| 1320 |
if (!app) { statusText("load a model first"); return; }
|
| 1321 |
$("templatePreview").textContent = "rendering…";
|
|
@@ -1538,15 +1725,26 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 1538 |
}
|
| 1539 |
}
|
| 1540 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1541 |
$("enableThinking").onchange = () => {
|
| 1542 |
const kw = readKwargs();
|
| 1543 |
if ($("enableThinking").checked) kw.enable_thinking = true;
|
| 1544 |
else delete kw.enable_thinking;
|
| 1545 |
$("templateKwargs").value = JSON.stringify(kw, null, 2);
|
|
|
|
| 1546 |
};
|
| 1547 |
|
| 1548 |
-
$("templateKwargs").value = "{}";
|
| 1549 |
-
|
| 1550 |
$("useMmproj").onchange = () => {
|
| 1551 |
$("mmprojSelect").disabled = !$("useMmproj").checked || !models.some((m) => m.multimodal);
|
| 1552 |
statusText($("useMmproj").checked
|
|
@@ -1554,6 +1752,16 @@ downloaded from a URL or read straight off your disk — never both.</small>
|
|
| 1554 |
: "no multimodal projector will be loaded");
|
| 1555 |
};
|
| 1556 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1557 |
// The threaded module needs SharedArrayBuffer, i.e. a cross-origin isolated
|
| 1558 |
// page. Without COOP/COEP the single-thread module is what the worker loads,
|
| 1559 |
// and asking for threads would only be a lie.
|
|
|
|
| 36 |
button:disabled { opacity: .45; cursor: default; }
|
| 37 |
#bar { height: 8px; background: #0d0f12; border: 1px solid #2a2f37; border-radius: 999px; overflow: hidden; margin: 10px 0 8px; }
|
| 38 |
#barFill { height: 100%; width: 0%; background: linear-gradient(90deg, #2f6fed, #5aa0ff); transition: width .15s linear; }
|
| 39 |
+
|
| 40 |
+
/* --- model ingest (Models tab) ---------------------------------------- */
|
| 41 |
+
/* separate from the engine bar on purpose: getting bytes into the library
|
| 42 |
+
and starting a context are different operations with different outcomes */
|
| 43 |
+
#ingest { border: 1px solid #2a2f37; border-radius: 8px; background: #171a1f;
|
| 44 |
+
padding: 9px 11px; margin: 0 0 12px; }
|
| 45 |
+
#ingest.error { border-color: #5a2530; }
|
| 46 |
+
.ingest-head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline;
|
| 47 |
+
font-size: 12px; color: #cfd4da; margin-bottom: 7px; }
|
| 48 |
+
#ingestPercent { color: #8a8f98; font-size: 11px; }
|
| 49 |
+
#ingestBar { height: 8px; background: #0d0f12; border: 1px solid #2a2f37;
|
| 50 |
+
border-radius: 999px; overflow: hidden; }
|
| 51 |
+
#ingestFill { height: 100%; width: 0%; background: linear-gradient(90deg, #6752c8, #a98bff);
|
| 52 |
+
transition: width .15s linear; }
|
| 53 |
+
#ingest.error #ingestFill { background: #c05050; }
|
| 54 |
#status { color: #9aa1ab; font-size: 13px; min-height: 20px; }
|
| 55 |
#out { white-space: pre-wrap; word-break: break-word; background: #0d0f12; border: 1px solid #2a2f37;
|
| 56 |
border-radius: 6px; padding: 12px; min-height: 140px; }
|
|
|
|
| 59 |
summary { cursor: pointer; color: #9aa1ab; font-size: 13px; }
|
| 60 |
#log { max-height: 180px; overflow: auto; background: #0d0f12; border: 1px solid #2a2f37; border-radius: 6px;
|
| 61 |
padding: 8px 10px; margin-top: 8px; font-size: 12px; color: #8a8f98; white-space: pre-wrap; }
|
| 62 |
+
#log .logline { display: block; }
|
| 63 |
+
#log .warn { color: #d8b25f; }
|
| 64 |
+
#log .err { color: #e07070; }
|
| 65 |
.pill { display: inline-block; padding: 1px 7px; border-radius: 999px; background: #23303f; color: #9cc3ff;
|
| 66 |
font-size: 11px; margin-left: 6px; }
|
| 67 |
|
| 68 |
/* --- top bar ---------------------------------------------------------- */
|
| 69 |
.appbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; }
|
| 70 |
.appbar label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #8a8f98; }
|
| 71 |
+
/* a flex item refuses to shrink below its longest option, so a long model name
|
| 72 |
+
would push the whole page sideways on a phone; min-width lets it ellipsize */
|
| 73 |
+
.appbar select { flex: 1 1 260px; width: auto; min-width: 0; max-width: 100%; }
|
| 74 |
.appbar button { padding: 7px 12px; font-size: 13px; }
|
| 75 |
#currentModel { flex: 1 1 100%; font-size: 12px; }
|
| 76 |
|
| 77 |
+
/* --- engine launch row (top bar) -------------------------------------- */
|
| 78 |
+
.enginebar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; margin-top: 8px;
|
| 79 |
+
padding: 7px 10px; border: 1px solid #2a2f37; border-radius: 8px; background: #171a1f; }
|
| 80 |
+
.enginebar-title { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #8a8f98; }
|
| 81 |
+
.ef { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #bfc5cd; white-space: nowrap; }
|
| 82 |
+
.ef input[type=number] { width: 66px; background: #0d0f12; color: #e6e6e6; border: 1px solid #2a2f37;
|
| 83 |
+
border-radius: 6px; padding: 3px 6px; font: inherit; font-size: 12px; }
|
| 84 |
+
.ef select { width: auto; background: #0d0f12; color: #e6e6e6; border: 1px solid #2a2f37;
|
| 85 |
+
border-radius: 6px; padding: 3px 5px; font: inherit; font-size: 12px; }
|
| 86 |
+
.enginebar #mmprojSelect { width: 118px; font-size: 12px; }
|
| 87 |
+
.ef.check { gap: 5px; }
|
| 88 |
+
.ef input[type=checkbox] { margin: 0; }
|
| 89 |
+
#threadsHint { flex: 1 1 100%; margin: 0; font-size: 11px; }
|
| 90 |
+
.enginebar details { flex: 1 1 100%; margin: 0; }
|
| 91 |
+
.enginebar summary { font-size: 11px; }
|
| 92 |
+
.enginebar details p { margin: 6px 0 0; }
|
| 93 |
+
.enginebar .hint { margin: 0; }
|
| 94 |
+
|
| 95 |
/* --- tabs ------------------------------------------------------------- */
|
| 96 |
.tabs { display: flex; flex-wrap: wrap; gap: 4px; margin: 18px 0 0; border-bottom: 1px solid #2a2f37; }
|
| 97 |
.tab { background: transparent; color: #9aa1ab; border: 1px solid transparent; border-bottom: 0;
|
|
|
|
| 224 |
<button id="stop" class="danger" disabled title="stop generating, keeping what has arrived">Stop</button>
|
| 225 |
<div id="currentModel" class="muted"></div>
|
| 226 |
</div>
|
| 227 |
+
<div class="enginebar" role="group" aria-label="engine launch parameters">
|
| 228 |
+
<span class="enginebar-title">engine <span class="pill" title="fixed when the context is created">launch</span></span>
|
| 229 |
+
<label class="ef" title="context length in tokens; KV memory grows with this">ctx
|
| 230 |
+
<input type="number" id="ctxSize" value="1024" step="256" min="128" max="16384"
|
| 231 |
+
aria-label="context length"></label>
|
| 232 |
+
<label class="ef">KV K <select id="kvTypeK" aria-label="KV cache type for K">
|
| 233 |
+
<option value="f16" selected>f16</option>
|
| 234 |
+
<option value="q8_0">q8_0</option>
|
| 235 |
+
<option value="q4_0">q4_0</option>
|
| 236 |
+
</select></label>
|
| 237 |
+
<label class="ef">KV V <select id="kvTypeV" aria-label="KV cache type for V">
|
| 238 |
+
<option value="f16" selected>f16</option>
|
| 239 |
+
<option value="q8_0">q8_0</option>
|
| 240 |
+
<option value="q4_0">q4_0</option>
|
| 241 |
+
</select></label>
|
| 242 |
+
<label class="ef">flash <select id="flashAttn" aria-label="flash attention">
|
| 243 |
+
<option value="-1" selected>auto</option>
|
| 244 |
+
<option value="0">off</option>
|
| 245 |
+
<option value="1">on</option>
|
| 246 |
+
</select></label>
|
| 247 |
+
<label class="ef">threads <input type="number" id="threads" min="1" max="16" step="1" value="1"
|
| 248 |
+
aria-label="CPU threads"></label>
|
| 249 |
+
<label class="ef check"><input type="checkbox" id="kvUnified" checked> unified KV</label>
|
| 250 |
+
<label class="ef check"><input type="checkbox" id="useMmproj"> mmproj</label>
|
| 251 |
+
<label class="ef">projector <select id="mmprojSelect" aria-label="multimodal projector" disabled></select></label>
|
| 252 |
+
<p class="hint" id="threadsHint"></p>
|
| 253 |
+
<details>
|
| 254 |
+
<summary>how launch settings apply</summary>
|
| 255 |
+
<p class="hint">Applied when a model loads — press <b>Reload engine</b> after changing them.
|
| 256 |
+
One parallel sequence: extra slots would only duplicate a cache a single user cannot fill.
|
| 257 |
+
A quantized V cache needs flash attention; asking for one switches it on. KV memory grows
|
| 258 |
+
with context length.</p>
|
| 259 |
+
<p class="hint">Flag a GGUF as <b>mmproj</b> in the model library to choose it here. Then attach
|
| 260 |
+
images to a message (button or drag & drop); the image goes into the prompt as the
|
| 261 |
+
projector's media marker.</p>
|
| 262 |
+
</details>
|
| 263 |
+
</div>
|
| 264 |
<div id="bar"><div id="barFill"></div></div>
|
| 265 |
<div id="status">idle</div>
|
| 266 |
|
|
|
|
| 314 |
</div>
|
| 315 |
|
| 316 |
<aside class="params" id="paramsRail">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
<h2>sampling</h2>
|
| 318 |
<label class="field">temperature <input type="number" id="temp" value="0.7" step="0.05" min="0" max="2"></label>
|
| 319 |
<label class="field">top_p <input type="number" id="topP" value="0.9" step="0.05" min="0" max="1"></label>
|
|
|
|
| 327 |
<label class="field wide">stop sequences
|
| 328 |
<textarea id="stopSequences" spellcheck="false" placeholder="one per line (\n and \t are expanded)"></textarea>
|
| 329 |
</label>
|
| 330 |
+
<h2>constrained output</h2>
|
| 331 |
+
<label class="field wide">constrain the reply with
|
| 332 |
+
<select id="constrain">
|
| 333 |
+
<option value="off">nothing (free text)</option>
|
| 334 |
+
<option value="tools" selected>tool calls (definitions below)</option>
|
| 335 |
+
<option value="json_schema">JSON schema</option>
|
| 336 |
+
<option value="grammar">GBNF grammar</option>
|
| 337 |
+
</select>
|
| 338 |
+
</label>
|
| 339 |
+
<label class="field wide" id="toolsWrap">tool definitions (OpenAI JSON)
|
| 340 |
<textarea id="tools" spellcheck="false" placeholder='[{"type":"function","function":{"name":"get_weather","parameters":{"type":"object","properties":{"city":{"type":"string"}}}}}]'></textarea>
|
| 341 |
</label>
|
| 342 |
+
<label class="field wide" id="schemaWrap" hidden>JSON schema
|
| 343 |
+
<textarea id="jsonSchema" spellcheck="false" placeholder='{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}'></textarea>
|
| 344 |
+
</label>
|
| 345 |
+
<label class="field wide" id="grammarWrap" hidden>GBNF grammar
|
| 346 |
+
<textarea id="grammar" spellcheck="false" placeholder='root ::= "yes" | "no"'></textarea>
|
| 347 |
+
</label>
|
| 348 |
+
<p class="hint" style="margin-top:8px">Tool definitions go to the chat template and the reply
|
| 349 |
+
is parsed by the model's own parser. A JSON schema or grammar instead constrains the reply
|
| 350 |
+
text itself, and replaces any grammar the template would have used. One constraint at a time.</p>
|
| 351 |
+
|
| 352 |
+
<h2>template</h2>
|
| 353 |
+
<label class="field wide">extra template arguments (JSON)
|
| 354 |
+
<textarea id="runTemplateKwargs" spellcheck="false"
|
| 355 |
+
placeholder='{"enable_thinking": true}'></textarea>
|
| 356 |
+
</label>
|
| 357 |
<label class="row"><input type="checkbox" id="enableThinking"> enable_thinking</label>
|
| 358 |
+
<p class="hint" style="margin-top:6px">The same object as <em>parameters JSON</em> on the Chat template
|
| 359 |
+
tab: edit it in either place — preview and generation both use it for the next turn.
|
| 360 |
+
<em>enable_thinking</em> only sets the template variable; whether reasoning is emitted and
|
| 361 |
+
separated into <code>reasoning_content</code> depends on the model's template and parser.</p>
|
| 362 |
</aside>
|
| 363 |
</div>
|
| 364 |
|
|
|
|
| 395 |
<section class="panel" id="panel-models" hidden>
|
| 396 |
<h2>model library</h2>
|
| 397 |
|
| 398 |
+
<div id="ingest" hidden>
|
| 399 |
+
<div class="ingest-head"><span id="ingestLabel">preparing…</span><span id="ingestPercent"></span></div>
|
| 400 |
+
<div id="ingestBar"><div id="ingestFill"></div></div>
|
| 401 |
+
</div>
|
| 402 |
+
|
| 403 |
<div class="add">
|
| 404 |
<h3>from a URL</h3>
|
| 405 |
<div class="addrow">
|
|
|
|
| 463 |
const pct = (loaded, total) => (total ? Math.min(100, (loaded / total) * 100) : 0);
|
| 464 |
|
| 465 |
const statusText = (t) => { $("status").textContent = t; };
|
| 466 |
+
// llama.cpp log levels: 1 debug, 2 info, 3 warn, 4 error, 5 continuation.
|
| 467 |
+
// Only warn/error are coloured; info is llama.cpp's own progress chatter
|
| 468 |
+
// (every image batch logs one line) and must not read as a failure.
|
| 469 |
+
const log = (t, level = 0) => {
|
| 470 |
const el = $("log");
|
| 471 |
+
const text = t.endsWith("\n") ? t : t + "\n";
|
| 472 |
+
if (level >= 3) {
|
| 473 |
+
const line = document.createElement("span");
|
| 474 |
+
line.className = "logline " + (level >= 4 ? "err" : "warn");
|
| 475 |
+
line.textContent = text;
|
| 476 |
+
el.appendChild(line);
|
| 477 |
+
} else {
|
| 478 |
+
el.appendChild(document.createTextNode(text));
|
| 479 |
+
}
|
| 480 |
el.scrollTop = el.scrollHeight;
|
| 481 |
};
|
| 482 |
const setBar = (loaded, total) => { $("barFill").style.width = pct(loaded, total).toFixed(1) + "%"; };
|
| 483 |
+
const resetBar = () => { $("barFill").style.width = "0%"; };
|
| 484 |
+
|
| 485 |
+
// Model ingest (download / copy into IndexedDB) has its own bar in the Models
|
| 486 |
+
// tab. The top bar tracks engine startup only; one bar for both made a finished
|
| 487 |
+
// download look like a starting engine and vice versa.
|
| 488 |
+
const showIngest = (label, loaded = 0, total = 0) => {
|
| 489 |
+
const box = $("ingest");
|
| 490 |
+
box.hidden = false;
|
| 491 |
+
box.classList.remove("error");
|
| 492 |
+
$("ingestLabel").textContent = label;
|
| 493 |
+
$("ingestPercent").textContent = total ? pct(loaded, total).toFixed(1) + "%" : "";
|
| 494 |
+
$("ingestFill").style.width = pct(loaded, total).toFixed(1) + "%";
|
| 495 |
+
};
|
| 496 |
+
const hideIngest = () => {
|
| 497 |
+
$("ingest").hidden = true;
|
| 498 |
+
$("ingest").classList.remove("error");
|
| 499 |
+
$("ingestFill").style.width = "0%";
|
| 500 |
+
};
|
| 501 |
+
const labelIngest = (label) => { if (!$("ingest").hidden) $("ingestLabel").textContent = label; };
|
| 502 |
+
const failIngest = (why) => {
|
| 503 |
+
$("ingest").hidden = false;
|
| 504 |
+
$("ingest").classList.add("error");
|
| 505 |
+
$("ingestLabel").textContent = "error: " + why;
|
| 506 |
+
$("ingestPercent").textContent = "";
|
| 507 |
+
};
|
| 508 |
|
| 509 |
let app = null;
|
| 510 |
let models = [];
|
| 511 |
let loadedId = null; // model currently in the wasm instance
|
| 512 |
+
let ingestingProjector = null; // name while an mmproj URL is being downloaded
|
| 513 |
let currentId = null; // model the user last asked to load
|
| 514 |
let pendingFile = null; // file picked for the "add" form
|
| 515 |
let reattachFor = null; // model id waiting for its file to be re-picked
|
|
|
|
| 623 |
stop: stopList(),
|
| 624 |
chat_template_source: templateOverride(),
|
| 625 |
tools: $("tools").value.trim(),
|
| 626 |
+
// the tool path above is untouched: these stay empty unless the picker
|
| 627 |
+
// selects them, and a caller grammar wins over the template's own
|
| 628 |
+
json_schema: $("constrain").value === "json_schema" ? $("jsonSchema").value.trim() : "",
|
| 629 |
+
grammar: $("constrain").value === "grammar" ? $("grammar").value.trim() : "",
|
| 630 |
template_kwargs: $("templateKwargs").value.trim() || "{}",
|
| 631 |
};
|
| 632 |
}
|
|
|
|
| 663 |
parent.postMessage({ type: "llmjs:ready", model: m.name,
|
| 664 |
n_ctx: info.n_ctx, threads: info.threads || 1 }, "*");
|
| 665 |
}
|
| 666 |
+
// the bar belongs to the startup, not to the loaded state
|
| 667 |
+
resetBar();
|
| 668 |
+
hideIngest();
|
| 669 |
setState("ready");
|
| 670 |
refreshModels();
|
| 671 |
},
|
|
|
|
| 743 |
chatRow.querySelector(".content").readOnly = false;
|
| 744 |
chatRow = null;
|
| 745 |
}
|
| 746 |
+
// an ingest failure belongs to the library surface, not the engine one
|
| 747 |
+
if (uiState === "downloading" || uiState === "copying" || ingestingProjector) {
|
| 748 |
+
failIngest(String(err));
|
| 749 |
+
ingestingProjector = null;
|
| 750 |
+
statusText("engine idle — the model library reported an error");
|
| 751 |
+
} else {
|
| 752 |
+
statusText("error: " + err);
|
| 753 |
+
}
|
| 754 |
log("ERROR: " + err);
|
| 755 |
setState(loadedId ? "ready" : "idle");
|
| 756 |
},
|
| 757 |
(p) => {
|
| 758 |
+
const done = p.total > 0 && p.loaded >= p.total;
|
| 759 |
if (p.phase === "download") {
|
| 760 |
+
// library work: the engine bar stays at zero until the bytes are here
|
| 761 |
setState("downloading");
|
| 762 |
+
resetBar();
|
| 763 |
+
showIngest(p.complete || done ? "downloaded — starting the engine…"
|
| 764 |
+
: `downloading ${fmt(p.loaded)} / ${fmt(p.total)}`, p.loaded, p.total);
|
| 765 |
} else if (p.phase === "copy") {
|
| 766 |
setState("copying");
|
| 767 |
+
resetBar();
|
| 768 |
+
if (done) hideIngest();
|
| 769 |
+
else showIngest(`copying into browser storage ${fmt(p.loaded)} / ${fmt(p.total)}`,
|
| 770 |
+
p.loaded, p.total);
|
| 771 |
} else if (p.phase === "hash") {
|
| 772 |
setState("hashing");
|
| 773 |
+
if (ingestingProjector) {
|
| 774 |
+
// no engine is starting: the projector is library content
|
| 775 |
+
resetBar();
|
| 776 |
+
showIngest(`verifying the projector (sha256 ${pct(p.loaded, p.total).toFixed(0)}%)`,
|
| 777 |
+
p.loaded, p.total);
|
| 778 |
+
statusText("engine idle — verifying the projector");
|
| 779 |
+
} else {
|
| 780 |
+
// the bytes are in the library; this is startup work
|
| 781 |
+
hideIngest();
|
| 782 |
+
statusText(`engine starting · verifying model bytes (sha256 ${pct(p.loaded, p.total).toFixed(0)}%)`);
|
| 783 |
+
setBar(p.loaded, p.total);
|
| 784 |
+
}
|
| 785 |
} else if (p.phase === "load") {
|
| 786 |
+
hideIngest();
|
| 787 |
setState("loading");
|
| 788 |
+
statusText(`engine starting · loading into wasm ${fmt(p.loaded)} / ${fmt(p.total)}`);
|
| 789 |
setBar(p.loaded, p.total);
|
| 790 |
}
|
| 791 |
},
|
| 792 |
+
(line, level) => log(line.trimEnd(), level ?? 0),
|
| 793 |
);
|
| 794 |
|
| 795 |
app.on_event = (msg) => {
|
|
|
|
| 801 |
$("fileLabel").value = "";
|
| 802 |
$("urlMmproj").checked = false;
|
| 803 |
$("fileMmproj").checked = false;
|
| 804 |
+
syncIngestButtons();
|
| 805 |
// adding a projector does not load anything, so nothing reports INITIALIZED
|
| 806 |
setState(loadedId ? "ready" : "idle");
|
| 807 |
refreshModels();
|
| 808 |
break;
|
| 809 |
|
| 810 |
case action.MODEL_UPDATED:
|
| 811 |
+
if (ingestingProjector) {
|
| 812 |
+
const name = ingestingProjector;
|
| 813 |
+
ingestingProjector = null;
|
| 814 |
+
hideIngest();
|
| 815 |
+
resetBar();
|
| 816 |
+
statusText(`${name} added to the model library — pick it in the engine row`);
|
| 817 |
+
setState(loadedId ? "ready" : "idle");
|
| 818 |
+
}
|
| 819 |
refreshModels();
|
| 820 |
break;
|
| 821 |
|
| 822 |
case action.PAUSED:
|
| 823 |
+
labelIngest("paused — press Resume to continue");
|
| 824 |
+
statusText("engine idle — the download is paused");
|
| 825 |
setState("paused");
|
| 826 |
refreshModels();
|
| 827 |
break;
|
|
|
|
| 1183 |
|
| 1184 |
function startLoad(m) {
|
| 1185 |
if (m.multimodal) {
|
| 1186 |
+
statusText(`${m.name} is an mmproj — tick mmproj in the engine row and pick it there`);
|
| 1187 |
return;
|
| 1188 |
}
|
| 1189 |
// a local file that was never copied needs its File again after a reload
|
|
|
|
| 1206 |
currentId = m.id;
|
| 1207 |
$("out").textContent = "";
|
| 1208 |
$("stats").textContent = "";
|
| 1209 |
+
resetBar();
|
| 1210 |
+
// a URL model that never finished is an ingest job first; its progress is
|
| 1211 |
+
// reported in the model library, and the engine bar starts at zero
|
| 1212 |
+
if (m.source === "url" && !m.complete) {
|
| 1213 |
+
showTab("models");
|
| 1214 |
+
showIngest("preparing download…");
|
| 1215 |
+
statusText("engine idle — download progress is in the model library");
|
| 1216 |
+
} else {
|
| 1217 |
+
hideIngest();
|
| 1218 |
+
statusText("starting…");
|
| 1219 |
+
}
|
| 1220 |
setState("starting");
|
| 1221 |
app.post({
|
| 1222 |
event: action.LOAD,
|
|
|
|
| 1236 |
if (!app || !currentId) return;
|
| 1237 |
$("out").textContent = "";
|
| 1238 |
$("stats").textContent = "";
|
| 1239 |
+
hideIngest();
|
| 1240 |
+
resetBar();
|
| 1241 |
statusText("freeing the model and reloading the engine…");
|
| 1242 |
setState("starting");
|
| 1243 |
const mpId = engineParams().mmproj_id;
|
|
|
|
| 1274 |
ensureApp();
|
| 1275 |
$("out").textContent = "";
|
| 1276 |
$("stats").textContent = "";
|
| 1277 |
+
resetBar();
|
| 1278 |
+
hideIngest();
|
| 1279 |
+
const copy = $("keepCopy").checked;
|
| 1280 |
+
const projector = $("fileMmproj").checked;
|
| 1281 |
+
if (projector) {
|
| 1282 |
+
statusText(`adding ${file.name} to the model library…`);
|
| 1283 |
+
if (copy) showIngest("copying the projector into the library…");
|
| 1284 |
+
} else if (copy) {
|
| 1285 |
+
showIngest("copying the file into the library…");
|
| 1286 |
+
statusText("engine idle — copy progress is in the model library");
|
| 1287 |
+
} else {
|
| 1288 |
+
statusText("starting…");
|
| 1289 |
+
}
|
| 1290 |
setState("starting");
|
| 1291 |
app.post({
|
| 1292 |
event: action.ADD_FILE,
|
|
|
|
| 1305 |
ensureApp();
|
| 1306 |
$("out").textContent = "";
|
| 1307 |
$("stats").textContent = "";
|
| 1308 |
+
resetBar();
|
| 1309 |
+
hideIngest();
|
| 1310 |
+
// progress lives in the library, so that is where the download happens
|
| 1311 |
+
showTab("models");
|
| 1312 |
+
// a projector is downloaded into the library and nothing is started: it
|
| 1313 |
+
// becomes a launch parameter of the next model load
|
| 1314 |
+
if (mmproj) ingestingProjector = name || url.split("/").pop() || "projector";
|
| 1315 |
+
showIngest("preparing download…");
|
| 1316 |
+
statusText("engine idle — download progress is in the model library");
|
| 1317 |
setState("starting");
|
| 1318 |
app.post({
|
| 1319 |
event: action.ADD_URL,
|
|
|
|
| 1457 |
}
|
| 1458 |
|
| 1459 |
$("pause").onclick = () => {
|
| 1460 |
+
if (app) { app.pause(); labelIngest("pausing…"); statusText("engine idle — pausing the download…"); }
|
| 1461 |
};
|
| 1462 |
|
| 1463 |
$("resume").onclick = () => {
|
| 1464 |
if (!app) return;
|
| 1465 |
+
labelIngest("resuming…");
|
| 1466 |
+
statusText("engine idle — resuming the download…");
|
| 1467 |
setState("downloading");
|
| 1468 |
app.resume();
|
| 1469 |
};
|
|
|
|
| 1493 |
syncTemplateUI();
|
| 1494 |
};
|
| 1495 |
|
| 1496 |
+
// one constraint at a time: the picker reveals only the box it applies to
|
| 1497 |
+
function syncConstrain() {
|
| 1498 |
+
const mode = $("constrain").value;
|
| 1499 |
+
$("toolsWrap").hidden = mode !== "tools";
|
| 1500 |
+
$("schemaWrap").hidden = mode !== "json_schema";
|
| 1501 |
+
$("grammarWrap").hidden = mode !== "grammar";
|
| 1502 |
+
}
|
| 1503 |
+
$("constrain").onchange = syncConstrain;
|
| 1504 |
+
syncConstrain();
|
| 1505 |
+
|
| 1506 |
$("previewTemplate").onclick = () => {
|
| 1507 |
if (!app) { statusText("load a model first"); return; }
|
| 1508 |
$("templatePreview").textContent = "rendering…";
|
|
|
|
| 1725 |
}
|
| 1726 |
}
|
| 1727 |
|
| 1728 |
+
// One object, two editors: the Chat template tab (for experiments with the
|
| 1729 |
+
// preview) and the inference rail (per-turn config). They mirror each other,
|
| 1730 |
+
// so whichever place the user edits, preview and generation both see it.
|
| 1731 |
+
const kwargBoxes = [$("templateKwargs"), $("runTemplateKwargs")];
|
| 1732 |
+
const syncKwargs = (src) => {
|
| 1733 |
+
for (const box of kwargBoxes) if (box !== src) box.value = src.value;
|
| 1734 |
+
};
|
| 1735 |
+
for (const box of kwargBoxes) box.addEventListener("input", () => syncKwargs(box));
|
| 1736 |
+
|
| 1737 |
+
$("templateKwargs").value = "{}";
|
| 1738 |
+
syncKwargs($("templateKwargs"));
|
| 1739 |
+
|
| 1740 |
$("enableThinking").onchange = () => {
|
| 1741 |
const kw = readKwargs();
|
| 1742 |
if ($("enableThinking").checked) kw.enable_thinking = true;
|
| 1743 |
else delete kw.enable_thinking;
|
| 1744 |
$("templateKwargs").value = JSON.stringify(kw, null, 2);
|
| 1745 |
+
syncKwargs($("templateKwargs"));
|
| 1746 |
};
|
| 1747 |
|
|
|
|
|
|
|
| 1748 |
$("useMmproj").onchange = () => {
|
| 1749 |
$("mmprojSelect").disabled = !$("useMmproj").checked || !models.some((m) => m.multimodal);
|
| 1750 |
statusText($("useMmproj").checked
|
|
|
|
| 1752 |
: "no multimodal projector will be loaded");
|
| 1753 |
};
|
| 1754 |
|
| 1755 |
+
// The button says what actually happens: a text model is downloaded and then
|
| 1756 |
+
// loaded, a projector is only put into the library (nothing to start).
|
| 1757 |
+
function syncIngestButtons() {
|
| 1758 |
+
$("addUrl").textContent = $("urlMmproj").checked ? "Download projector" : "Download & load";
|
| 1759 |
+
$("addFile").textContent = $("fileMmproj").checked ? "Add to library" : "Add & load";
|
| 1760 |
+
}
|
| 1761 |
+
$("urlMmproj").onchange = syncIngestButtons;
|
| 1762 |
+
$("fileMmproj").onchange = syncIngestButtons;
|
| 1763 |
+
syncIngestButtons();
|
| 1764 |
+
|
| 1765 |
// The threaded module needs SharedArrayBuffer, i.e. a cross-origin isolated
|
| 1766 |
// page. Without COOP/COEP the single-thread module is what the worker loads,
|
| 1767 |
// and asking for threads would only be a lie.
|
llm.js
CHANGED
|
@@ -95,7 +95,9 @@ class LLM {
|
|
| 95 |
break;
|
| 96 |
|
| 97 |
case action.LOG:
|
| 98 |
-
|
|
|
|
|
|
|
| 99 |
break;
|
| 100 |
|
| 101 |
case action.ERROR:
|
|
|
|
| 95 |
break;
|
| 96 |
|
| 97 |
case action.LOG:
|
| 98 |
+
// the level decides what is an error and what is llama.cpp
|
| 99 |
+
// chatter: dropping it made INFO lines look like failures
|
| 100 |
+
if (this.on_log_callback) this.on_log_callback(data.line, data.level ?? 0);
|
| 101 |
break;
|
| 102 |
|
| 103 |
case action.ERROR:
|
wasm/llm-wasm-mt.build.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
llama.cpp commit: f7591a6a01f9273de03331732bdc0b39721168be
|
| 2 |
llama.cpp subject: sycl: add f16 support for SWIGLU_OAI (#30857)
|
| 3 |
emcc: emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 7.0.0 (777e6e5db7b4990bcaeb1b9998ed642cc84e6629)
|
| 4 |
-
built: 2026-11-
|
|
|
|
| 1 |
llama.cpp commit: f7591a6a01f9273de03331732bdc0b39721168be
|
| 2 |
llama.cpp subject: sycl: add f16 support for SWIGLU_OAI (#30857)
|
| 3 |
emcc: emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 7.0.0 (777e6e5db7b4990bcaeb1b9998ed642cc84e6629)
|
| 4 |
+
built: 2026-11-10T16:49:06Z
|
wasm/llm-wasm-mt.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
wasm/llm-wasm-mt.wasm
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1d34dbe1230fdb116e0aede29c9559e09195a93987883f288de06bae3e5908c3
|
| 3 |
+
size 7040863
|
worker.js
CHANGED
|
@@ -115,7 +115,11 @@ async function ensureModule() {
|
|
| 115 |
}
|
| 116 |
}
|
| 117 |
if (!factory) factory = (await import("./wasm/llm-wasm.js")).default;
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
module._llmjs_init();
|
| 120 |
log(`llama.cpp ${module.UTF8ToString(module._llmjs_version())}` +
|
| 121 |
(THREADED ? " (threaded build)" : " (single-thread build)"));
|
|
@@ -255,7 +259,11 @@ async function loadIntoFS(model, target = "/models/model.gguf") {
|
|
| 255 |
return written;
|
| 256 |
}
|
| 257 |
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
if (busy) {
|
| 260 |
fail(new Error("a load is already running"));
|
| 261 |
return;
|
|
@@ -270,25 +278,13 @@ async function start({ id, file, mmproj_file, context_size, kv, mmproj_id, threa
|
|
| 270 |
|
| 271 |
busy = true;
|
| 272 |
abort = new AbortController();
|
| 273 |
-
lastRequest = { id, context_size, kv: { ...currentKv }, mmproj_id, threads: currentThreads
|
|
|
|
| 274 |
|
| 275 |
try {
|
| 276 |
-
await ensureModule();
|
| 277 |
-
// older single-thread modules predate this export; they clamp to 1 anyway
|
| 278 |
-
if (module._llmjs_set_threads) {
|
| 279 |
-
module.ccall("llmjs_set_threads", null, ["number"], [currentThreads]);
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
let model = await getModel(id);
|
| 283 |
if (!model) throw new Error(`unknown model ${id}`);
|
| 284 |
|
| 285 |
-
// the wasm instance can only hold one model at a time
|
| 286 |
-
if (loaded && loadedId !== id) {
|
| 287 |
-
module._llmjs_free();
|
| 288 |
-
loaded = false;
|
| 289 |
-
loadedId = null;
|
| 290 |
-
}
|
| 291 |
-
|
| 292 |
// 1. get the bytes into the browser (url models only; a local file is
|
| 293 |
// already where it needs to be)
|
| 294 |
if (model.source === "url" && !model.complete) {
|
|
@@ -305,7 +301,26 @@ async function start({ id, file, mmproj_file, context_size, kv, mmproj_id, threa
|
|
| 305 |
// 2. identify the bytes once
|
| 306 |
if (!model.sha256) model = await hashModel(model);
|
| 307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
// 3. into the wasm FS, then load
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
const bytes = await loadIntoFS(model);
|
| 310 |
if (model.received && bytes !== model.received) {
|
| 311 |
log(`stored model size ${bytes} != expected ${model.received}`);
|
|
@@ -399,6 +414,7 @@ async function run(opts) {
|
|
| 399 |
tools = "[]",
|
| 400 |
template_kwargs = "{}",
|
| 401 |
grammar = "",
|
|
|
|
| 402 |
images = [],
|
| 403 |
} = opts;
|
| 404 |
|
|
@@ -411,6 +427,18 @@ async function run(opts) {
|
|
| 411 |
return;
|
| 412 |
}
|
| 413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
if (createContext(context_size || currentNctx) < 0) {
|
| 415 |
fail(new Error("could not create the inference context"));
|
| 416 |
return;
|
|
@@ -450,7 +478,10 @@ async function run(opts) {
|
|
| 450 |
);
|
| 451 |
}
|
| 452 |
if (rc < 0) {
|
| 453 |
-
|
|
|
|
|
|
|
|
|
|
| 454 |
return;
|
| 455 |
}
|
| 456 |
|
|
@@ -508,10 +539,11 @@ self.addEventListener("message", (e) => {
|
|
| 508 |
case action.ADD_URL:
|
| 509 |
(async () => {
|
| 510 |
const id = await addUrlModel(msg);
|
| 511 |
-
// a projector is not a model to run:
|
| 512 |
-
//
|
| 513 |
-
|
| 514 |
-
await start({ id, context_size: msg.context_size, kv: msg.kv, threads: msg.threads
|
|
|
|
| 515 |
})().catch(fail);
|
| 516 |
break;
|
| 517 |
|
|
|
|
| 115 |
}
|
| 116 |
}
|
| 117 |
if (!factory) factory = (await import("./wasm/llm-wasm.js")).default;
|
| 118 |
+
// emscripten writes to stdout/stderr for anything that does not go
|
| 119 |
+
// through our log callback; that is chatter, not a failure, so it
|
| 120 |
+
// must not land in the console as an error
|
| 121 |
+
const print = (text) => self.llmjs_on_log(2, String(text).replace(/\n?$/, "\n"));
|
| 122 |
+
module = await factory({ print, printErr: print });
|
| 123 |
module._llmjs_init();
|
| 124 |
log(`llama.cpp ${module.UTF8ToString(module._llmjs_version())}` +
|
| 125 |
(THREADED ? " (threaded build)" : " (single-thread build)"));
|
|
|
|
| 259 |
return written;
|
| 260 |
}
|
| 261 |
|
| 262 |
+
// `ingest_only` stops after the bytes are in the library (download + hash): a
|
| 263 |
+
// projector is not something the engine can "start", it is picked as a launch
|
| 264 |
+
// parameter of the next model load.
|
| 265 |
+
async function start({ id, file, mmproj_file, context_size, kv, mmproj_id, threads,
|
| 266 |
+
ingest_only = false }) {
|
| 267 |
if (busy) {
|
| 268 |
fail(new Error("a load is already running"));
|
| 269 |
return;
|
|
|
|
| 278 |
|
| 279 |
busy = true;
|
| 280 |
abort = new AbortController();
|
| 281 |
+
lastRequest = { id, context_size, kv: { ...currentKv }, mmproj_id, threads: currentThreads,
|
| 282 |
+
ingest_only };
|
| 283 |
|
| 284 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
let model = await getModel(id);
|
| 286 |
if (!model) throw new Error(`unknown model ${id}`);
|
| 287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
// 1. get the bytes into the browser (url models only; a local file is
|
| 289 |
// already where it needs to be)
|
| 290 |
if (model.source === "url" && !model.complete) {
|
|
|
|
| 301 |
// 2. identify the bytes once
|
| 302 |
if (!model.sha256) model = await hashModel(model);
|
| 303 |
|
| 304 |
+
if (ingest_only) {
|
| 305 |
+
busy = false;
|
| 306 |
+
post({ event: action.MODEL_UPDATED, model });
|
| 307 |
+
return;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
// 3. into the wasm FS, then load
|
| 311 |
+
await ensureModule();
|
| 312 |
+
// older single-thread modules predate this export; they clamp to 1 anyway
|
| 313 |
+
if (module._llmjs_set_threads) {
|
| 314 |
+
module.ccall("llmjs_set_threads", null, ["number"], [currentThreads]);
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
// the wasm instance can only hold one model at a time
|
| 318 |
+
if (loaded && loadedId !== id) {
|
| 319 |
+
module._llmjs_free();
|
| 320 |
+
loaded = false;
|
| 321 |
+
loadedId = null;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
const bytes = await loadIntoFS(model);
|
| 325 |
if (model.received && bytes !== model.received) {
|
| 326 |
log(`stored model size ${bytes} != expected ${model.received}`);
|
|
|
|
| 414 |
tools = "[]",
|
| 415 |
template_kwargs = "{}",
|
| 416 |
grammar = "",
|
| 417 |
+
json_schema = "",
|
| 418 |
images = [],
|
| 419 |
} = opts;
|
| 420 |
|
|
|
|
| 427 |
return;
|
| 428 |
}
|
| 429 |
|
| 430 |
+
// structured output: only when the page chose a schema. The tool-call path
|
| 431 |
+
// sends neither, so it runs exactly as before.
|
| 432 |
+
if (!grammar && json_schema && json_schema.trim()) {
|
| 433 |
+
const asGrammar = module.ccall("llmjs_grammar_from_schema", "string",
|
| 434 |
+
["string"], [json_schema]);
|
| 435 |
+
if (!asGrammar) {
|
| 436 |
+
fail(new Error(`could not use the JSON schema: ${module.UTF8ToString(module._llmjs_last_error())}`));
|
| 437 |
+
return;
|
| 438 |
+
}
|
| 439 |
+
grammar = asGrammar;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
if (createContext(context_size || currentNctx) < 0) {
|
| 443 |
fail(new Error("could not create the inference context"));
|
| 444 |
return;
|
|
|
|
| 478 |
);
|
| 479 |
}
|
| 480 |
if (rc < 0) {
|
| 481 |
+
// the engine knows why: an image larger than the context, a marker/image
|
| 482 |
+
// mismatch, no projector … - "(-1)" alone tells the user nothing
|
| 483 |
+
const why = module.UTF8ToString(module._llmjs_last_error());
|
| 484 |
+
fail(new Error(`could not start generation (${rc})${why ? `: ${why}` : ""}`));
|
| 485 |
return;
|
| 486 |
}
|
| 487 |
|
|
|
|
| 539 |
case action.ADD_URL:
|
| 540 |
(async () => {
|
| 541 |
const id = await addUrlModel(msg);
|
| 542 |
+
// a projector is not a model to run: download its bytes into the
|
| 543 |
+
// library, then stop - it is picked in the engine parameters and
|
| 544 |
+
// loaded next to whatever model is selected
|
| 545 |
+
await start({ id, context_size: msg.context_size, kv: msg.kv, threads: msg.threads,
|
| 546 |
+
ingest_only: !!msg.mmproj });
|
| 547 |
})().catch(fail);
|
| 548 |
break;
|
| 549 |
|