@clutor/cli 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/c/Makefile +46 -2
- package/c/src/forward.c +415 -0
- package/c/src/forward.h +76 -0
- package/c/src/main.c +337 -6
- package/c/src/meta.c +118 -0
- package/c/src/meta.h +22 -0
- package/c/src/quant.c +360 -0
- package/c/src/quant.h +43 -0
- package/c/src/stream.c +6 -3
- package/c/src/stream.h +2 -0
- package/c/src/tokenizer.c +296 -0
- package/c/src/tokenizer.h +25 -0
- package/c/src/xs.c +215 -0
- package/c/src/xs.h +39 -0
- package/dist/bin.js +2 -1
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-RBSFKJ6L.js → chunk-BZZUO7XX.js} +35 -6
- package/dist/chunk-BZZUO7XX.js.map +1 -0
- package/dist/{chunk-YL63VENN.js → chunk-ODS6ND7T.js} +241 -33
- package/dist/chunk-ODS6ND7T.js.map +1 -0
- package/dist/index.d.ts +26 -7
- package/dist/index.js +8 -2
- package/dist/{repl-OVODHQI5.js → repl-DOWMDXFR.js} +2 -2
- package/package.json +2 -2
- package/dist/chunk-RBSFKJ6L.js.map +0 -1
- package/dist/chunk-YL63VENN.js.map +0 -1
- /package/dist/{repl-OVODHQI5.js.map → repl-DOWMDXFR.js.map} +0 -0
|
@@ -6,6 +6,38 @@ import {
|
|
|
6
6
|
|
|
7
7
|
// src/repl.ts
|
|
8
8
|
import { TOOLS } from "@clutor/core";
|
|
9
|
+
|
|
10
|
+
// src/banner.ts
|
|
11
|
+
var GLYPHS = {
|
|
12
|
+
C: [" \u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D", "\u2588\u2588\u2551 ", "\u2588\u2588\u2551 ", "\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557", " \u255A\u2550\u2550\u2550\u2550\u2550\u255D"],
|
|
13
|
+
L: ["\u2588\u2588\u2557 ", "\u2588\u2588\u2551 ", "\u2588\u2588\u2551 ", "\u2588\u2588\u2551 ", "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"],
|
|
14
|
+
U: ["\u2588\u2588\u2557 \u2588\u2588\u2557", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D", " \u255A\u2550\u2550\u2550\u2550\u2550\u255D "],
|
|
15
|
+
T: ["\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D", " \u2588\u2588\u2551 ", " \u2588\u2588\u2551 ", " \u2588\u2588\u2551 ", " \u255A\u2550\u255D "],
|
|
16
|
+
O: [" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ", "\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D", " \u255A\u2550\u2550\u2550\u2550\u2550\u255D "],
|
|
17
|
+
R: ["\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557", "\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557", "\u2588\u2588\u2551 \u2588\u2588\u2551", "\u255A\u2550\u255D \u255A\u2550\u255D"]
|
|
18
|
+
};
|
|
19
|
+
var WORDMARK = Array.from({ length: 6 }, (_, row) => [..."CLUTOR"].map((ch) => GLYPHS[ch]?.[row] ?? "").join(""));
|
|
20
|
+
var WORDMARK_WIDTH = Math.max(...WORDMARK.map((r) => r.length));
|
|
21
|
+
var GRADIENT = [51, 45, 39, 33, 63, 99];
|
|
22
|
+
function useColor(env = process.env, isTTY = !!process.stdout.isTTY) {
|
|
23
|
+
return isTTY && !("NO_COLOR" in env) && env["TERM"] !== "dumb";
|
|
24
|
+
}
|
|
25
|
+
function banner(o) {
|
|
26
|
+
const paint = (code, s) => o.color ? `\x1B[${typeof code === "number" ? `38;5;${code}` : code}m${s}\x1B[0m` : s;
|
|
27
|
+
const dim = (s) => paint("2", s);
|
|
28
|
+
const wide = (o.columns ?? 80) >= WORDMARK_WIDTH + 2;
|
|
29
|
+
const lines = [""];
|
|
30
|
+
if (wide) lines.push(...WORDMARK.map((r, i) => paint(GRADIENT[i] ?? 99, r)));
|
|
31
|
+
else lines.push(paint(45, "C L U T O R"));
|
|
32
|
+
lines.push("", `${paint("1", "clutor interactive session")} ${dim(`v${o.version}`)}`, dim("big models on small machines, with honest numbers"), "");
|
|
33
|
+
lines.push(` ${dim("folder")} ${o.folder}`, ` ${dim("model")} ${o.model}`, "");
|
|
34
|
+
lines.push(` ${dim("tips")} type a task in plain words and the agent works on this folder`);
|
|
35
|
+
lines.push(` its changes are held aside; nothing is written without your approval`);
|
|
36
|
+
lines.push(` /help lists commands \xB7 /plan <model> \xB7 /cache \xB7 /exit`, "");
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/repl.ts
|
|
9
41
|
var FORWARD = {
|
|
10
42
|
status: { usage: "/status", help: "machine, model backend, memory plan, receipts" },
|
|
11
43
|
doctor: { usage: "/doctor", help: "check this machine and the configuration" },
|
|
@@ -60,7 +92,7 @@ function helpText() {
|
|
|
60
92
|
|
|
61
93
|
${table(rows)}`;
|
|
62
94
|
}
|
|
63
|
-
async function runRepl(io, run, engine) {
|
|
95
|
+
async function runRepl(io, run, engine, version = "dev") {
|
|
64
96
|
const ask = io.prompt;
|
|
65
97
|
const session = { auto: false, notes: [], usage: { promptTokens: 0, completionTokens: 0 } };
|
|
66
98
|
const load = () => {
|
|
@@ -73,10 +105,7 @@ async function runRepl(io, run, engine) {
|
|
|
73
105
|
};
|
|
74
106
|
const cfg = load();
|
|
75
107
|
const model = (c) => !c ? "not configured" : c.backend.kind === "openai" ? `${c.backend.model ?? "default"} at ${c.backend.baseUrl}` : c.backend.llamaServer.model ?? "not configured (set backend.llamaServer.model)";
|
|
76
|
-
io.out(
|
|
77
|
-
folder ${io.cwd}
|
|
78
|
-
model ${model(cfg)}
|
|
79
|
-
Type a task, or /help for commands. Nothing is written to your files without your approval.`);
|
|
108
|
+
io.out(banner({ version, folder: io.cwd, model: model(cfg), columns: process.stdout.columns ?? 80, color: useColor() }));
|
|
80
109
|
try {
|
|
81
110
|
for (; ; ) {
|
|
82
111
|
const line = (await ask("clt> ")).trim();
|
|
@@ -175,4 +204,4 @@ export {
|
|
|
175
204
|
splitWords,
|
|
176
205
|
runRepl
|
|
177
206
|
};
|
|
178
|
-
//# sourceMappingURL=chunk-
|
|
207
|
+
//# sourceMappingURL=chunk-BZZUO7XX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/repl.ts","../src/banner.ts"],"sourcesContent":["import { TOOLS } from \"@clutor/core\";\nimport { loadConfig, ConfigError, type ClutorConfig } from \"./config\";\nimport { banner, useColor } from \"./banner\";\nimport { table } from \"./format\";\nimport type { Io, SessionState } from \"./cli\";\n\n/**\n * The interactive session that `clutor` opens when it is started with no command in a terminal. Text you type is a task for the coding\n * agent (its changes are held aside until you approve them); a line starting with `/` is a command. Most commands are the ordinary\n * clutor commands, so they behave exactly as they do outside the session.\n */\n\ntype Run = (argv: string[], session: SessionState) => Promise<number>;\ntype EngineView = (config: ClutorConfig, what: \"layers\" | \"cache\") => Promise<void>;\n\ninterface Slash { usage: string; help: string }\n\n/** Commands that are forwarded unchanged to the normal CLI. */\nconst FORWARD: Record<string, Slash> = {\n status: { usage: \"/status\", help: \"machine, model backend, memory plan, receipts\" },\n doctor: { usage: \"/doctor\", help: \"check this machine and the configuration\" },\n models: { usage: \"/models\", help: \"model catalog and the .gguf files in your models folder\" },\n plan: { usage: \"/plan <model> [--ram GiB]\", help: \"will it fit, where the memory goes, how fast it could possibly run\" },\n simulate: { usage: \"/simulate <model> [--cache GiB]\", help: \"replay a routing trace through the expert cache (synthetic)\" },\n inspect: { usage: \"/inspect <file.gguf>\", help: \"read a model file's header\" },\n bench: { usage: \"/bench [--file model.gguf]\", help: \"measure real disk reads; with the engine, compare cache strategies\" },\n stream: { usage: \"/stream <model.gguf>\", help: \"stream a model's experts through the native engine (synthetic routing)\" },\n pull: { usage: \"/pull [id]\", help: \"list or download a small test model\" },\n init: { usage: \"/init\", help: \"create clutor.config.json and a models folder here\" },\n backend: { usage: \"/backend\", help: \"show how llama-server would be started\" },\n ledger: { usage: \"/ledger\", help: \"verify the receipt log and list recent tasks and checkpoints\" },\n decide: { usage: '/decide --prompt \"<text>\" --choices \"a,b\"', help: \"one forward pass: probability of each choice\" },\n};\n\nconst OWN: Record<string, Slash> = {\n help: { usage: \"/help\", help: \"this list\" },\n model: { usage: \"/model\", help: \"which model this session talks to\" },\n verify: { usage: \"/verify [command|off]\", help: \"run this check after every task and only offer changes that pass it\" },\n yes: { usage: \"/yes\", help: \"toggle: run project commands without asking (only for projects you trust)\" },\n undo: { usage: \"/undo\", help: \"restore the files changed by the last applied task\" },\n tools: { usage: \"/tools\", help: \"what the agent can do\" },\n cost: { usage: \"/cost\", help: \"tokens used this session, as reported by the model server\" },\n history: { usage: \"/history\", help: \"tasks done in this session\" },\n clear: { usage: \"/clear\", help: \"forget this session's task history\" },\n layers: { usage: \"/layers\", help: \"where the model file's bytes are, per layer (native engine)\" },\n cache: { usage: \"/cache\", help: \"a streaming pass over the model file: cache, prefetch, disk waits\" },\n exit: { usage: \"/exit\", help: \"leave (also /quit, Ctrl-D)\" },\n};\n\n/** Splits a line into words, honouring single and double quotes. */\nexport function splitWords(line: string): string[] {\n const out: string[] = [];\n let cur = \"\", quote: string | undefined, has = false;\n for (const ch of line) {\n if (quote) { if (ch === quote) quote = undefined; else cur += ch; }\n else if (ch === '\"' || ch === \"'\") { quote = ch; has = true; }\n else if (/\\s/.test(ch)) { if (cur || has) out.push(cur); cur = \"\"; has = false; }\n else cur += ch;\n }\n if (cur || has) out.push(cur);\n return out;\n}\n\nfunction helpText(): string {\n const rows = [...Object.values(OWN), ...Object.values(FORWARD)].map((c) => [c.usage, c.help]);\n return `Type a task in plain words and the agent works on this folder. Its changes are held aside; you see the diff and approve before anything is written.\\n\\n${table(rows)}`;\n}\n\nexport async function runRepl(io: Io, run: Run, engine: EngineView, version = \"dev\"): Promise<number> {\n const ask = io.prompt as NonNullable<Io[\"prompt\"]>;\n const session: SessionState = { auto: false, notes: [], usage: { promptTokens: 0, completionTokens: 0 } };\n const load = (): ClutorConfig | undefined => {\n try { return loadConfig(io.cwd).config; } catch (e) { io.err(e instanceof ConfigError || e instanceof Error ? e.message : String(e)); return undefined; }\n };\n const cfg = load();\n const model = (c: ClutorConfig | undefined): string => !c ? \"not configured\" : c.backend.kind === \"openai\" ? `${c.backend.model ?? \"default\"} at ${c.backend.baseUrl}` : c.backend.llamaServer.model ?? \"not configured (set backend.llamaServer.model)\";\n io.out(banner({ version, folder: io.cwd, model: model(cfg), columns: process.stdout.columns ?? 80, color: useColor() }));\n\n try {\n for (;;) {\n const line = (await ask(\"clt> \")).trim();\n if (!line) continue;\n if (!line.startsWith(\"/\")) { await guarded(io, () => run([\"code\", line, \"--dir\", io.cwd], session)); continue; }\n\n const [word = \"\", ...rest] = splitWords(line.slice(1));\n const name = word.toLowerCase();\n if (name === \"exit\" || name === \"quit\") return 0;\n if (name === \"help\" || name === \"?\") { io.out(helpText()); continue; }\n if (name === \"model\") { io.out(`model: ${model(load())}${session.backend ? \" (running)\" : \"\"}\\nverify: ${session.verify ?? \"none\"} auto-approve: ${session.auto ? \"on\" : \"off\"}`); continue; }\n if (name === \"verify\") {\n const v = rest.join(\" \").trim();\n if (!v) io.out(session.verify ? `verify: ${session.verify}` : \"verify: none. Set one with /verify npm test\");\n else if (v === \"off\") { delete session.verify; io.out(\"verify: off\"); }\n else { session.verify = v; io.out(`verify: ${v}. It runs in a scratch copy after each task; changes are only offered when it passes.`); }\n continue;\n }\n if (name === \"yes\") { session.auto = !session.auto; io.out(session.auto ? \"auto-approve ON: commands from the agent run without asking.\" : \"auto-approve off.\"); continue; }\n if (name === \"undo\") {\n if (!session.lastCheckpoint) { io.out(\"Nothing to undo in this session.\"); continue; }\n const { id, dir } = session.lastCheckpoint;\n if (await guarded(io, () => run([\"rollback\", id, \"--dir\", dir], session)) === 0) delete session.lastCheckpoint;\n continue;\n }\n if (name === \"tools\") { io.out(table(TOOLS.map((t) => [t.name, t.description.split(\". \")[0] ?? \"\"]))); continue; }\n if (name === \"cost\") {\n const u = session.usage;\n io.out(`${u.promptTokens} prompt + ${u.completionTokens} generated tokens this session (reported by the model server; local models cost no money).`);\n continue;\n }\n if (name === \"history\") { io.out(session.notes.length ? session.notes.map((n, i) => `${i + 1}. ${n}`).join(\"\\n\") : \"No tasks yet.\"); continue; }\n if (name === \"clear\") { session.notes.length = 0; io.out(\"(session history cleared)\"); continue; }\n if (name === \"layers\" || name === \"cache\") {\n const c = load();\n if (c) await guarded(io, async () => { await engine(c, name); return 0; });\n continue;\n }\n if (FORWARD[name]) { await guarded(io, () => run([name, ...rest], session)); continue; }\n io.err(`unknown command /${word}. /help lists them.`);\n }\n } finally {\n try { await session.backend?.stop(); } catch { /* the server may already be gone */ }\n }\n}\n\nasync function guarded(io: Io, f: () => Promise<number>): Promise<number | undefined> {\n try { return await f(); } catch (e) { io.err(e instanceof Error ? e.message : String(e)); return undefined; }\n}\n","/**\n * The welcome screen of the interactive session: the name in large block letters, then where you are and what to type.\n * Colour is used only on a terminal that asked for it (not when piped, not with NO_COLOR); the text is the same either way.\n */\n\nconst GLYPHS: Record<string, string[]> = {\n C: [\" ██████╗\", \"██╔════╝\", \"██║ \", \"██║ \", \"╚██████╗\", \" ╚═════╝\"],\n L: [\"██╗ \", \"██║ \", \"██║ \", \"██║ \", \"███████╗\", \"╚══════╝\"],\n U: [\"██╗ ██╗\", \"██║ ██║\", \"██║ ██║\", \"██║ ██║\", \"╚██████╔╝\", \" ╚═════╝ \"],\n T: [\"████████╗\", \"╚══██╔══╝\", \" ██║ \", \" ██║ \", \" ██║ \", \" ╚═╝ \"],\n O: [\" ██████╗ \", \"██╔═══██╗\", \"██║ ██║\", \"██║ ██║\", \"╚██████╔╝\", \" ╚═════╝ \"],\n R: [\"██████╗ \", \"██╔══██╗\", \"██████╔╝\", \"██╔══██╗\", \"██║ ██║\", \"╚═╝ ╚═╝\"],\n};\n\n/** The six rows of \"CLUTOR\" in block letters. */\nexport const WORDMARK: string[] = Array.from({ length: 6 }, (_, row) => [...\"CLUTOR\"].map((ch) => GLYPHS[ch]?.[row] ?? \"\").join(\"\"));\nexport const WORDMARK_WIDTH = Math.max(...WORDMARK.map((r) => r.length));\n\n/** Top to bottom: cyan, through blue, to violet (xterm 256-colour codes). */\nconst GRADIENT = [51, 45, 39, 33, 63, 99];\n\nexport interface BannerOptions {\n version: string;\n folder: string;\n model: string;\n /** Width of the terminal in columns; a narrower terminal than the block letters gets a one-line title. */\n columns?: number;\n color?: boolean;\n}\n\nexport function useColor(env: NodeJS.ProcessEnv = process.env, isTTY: boolean = !!process.stdout.isTTY): boolean {\n return isTTY && !(\"NO_COLOR\" in env) && env[\"TERM\"] !== \"dumb\";\n}\n\nexport function banner(o: BannerOptions): string {\n const paint = (code: number | string, s: string): string => (o.color ? `\\x1b[${typeof code === \"number\" ? `38;5;${code}` : code}m${s}\\x1b[0m` : s);\n const dim = (s: string): string => paint(\"2\", s);\n const wide = (o.columns ?? 80) >= WORDMARK_WIDTH + 2;\n const lines: string[] = [\"\"];\n if (wide) lines.push(...WORDMARK.map((r, i) => paint(GRADIENT[i] ?? 99, r)));\n else lines.push(paint(45, \"C L U T O R\"));\n lines.push(\"\", `${paint(\"1\", \"clutor interactive session\")} ${dim(`v${o.version}`)}`, dim(\"big models on small machines, with honest numbers\"), \"\");\n lines.push(` ${dim(\"folder\")} ${o.folder}`, ` ${dim(\"model\")} ${o.model}`, \"\");\n lines.push(` ${dim(\"tips\")} type a task in plain words and the agent works on this folder`);\n lines.push(` its changes are held aside; nothing is written without your approval`);\n lines.push(` /help lists commands · /plan <model> · /cache · /exit`, \"\");\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -320,7 +320,8 @@ function withPrefixSnapshots(inner, snaps) {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
// src/models.ts
|
|
323
|
-
import {
|
|
323
|
+
import { createHash as createHash3 } from "crypto";
|
|
324
|
+
import { createReadStream, createWriteStream } from "fs";
|
|
324
325
|
import { mkdir, readdir, rename, rm, stat, statfs } from "fs/promises";
|
|
325
326
|
import { Readable } from "stream";
|
|
326
327
|
import { pipeline } from "stream/promises";
|
|
@@ -380,8 +381,11 @@ async function listLocalModels(dir) {
|
|
|
380
381
|
}
|
|
381
382
|
var GiB_ = 2 ** 30;
|
|
382
383
|
var DOWNLOADS = [
|
|
383
|
-
{ id: "olmoe-1b-7b-q4_k_m", catalogId: "olmoe-1b-7b", repo: "
|
|
384
|
-
{ id: "
|
|
384
|
+
{ id: "olmoe-1b-7b-q4_k_m", catalogId: "olmoe-1b-7b", repo: "allenai/OLMoE-1B-7B-0924-Instruct-GGUF", file: "olmoe-1b-7b-0924-instruct-q4_k_m.gguf", bytes: 4213512672, sha256: "8c310f1435a1222338fd2d3d974975be9cd908180b644bab0c2a94da1ac32f3f", note: "a real mixture-of-experts model (64 experts, 8 used) that fits in memory: the one `clutor generate` was validated on" },
|
|
385
|
+
{ id: "granite-3.0-3b-a800m-q4_k_m", catalogId: "granite-3.0-3b-a800m", repo: "bartowski/granite-3.0-3b-a800m-instruct-GGUF", file: "granite-3.0-3b-a800m-instruct-Q4_K_M.gguf", bytes: 2059356e3, sha256: "aa6287bafd182d06a6d11b0d001458deec902e8c0522875fc20cde2f561ebf6d", note: "IBM Granite MoE (40 experts, 8 used, 1.9 GiB): the third architecture the engine can run with `clutor generate`" },
|
|
386
|
+
{ id: "qwen3-30b-a3b-q4_k_m", catalogId: "qwen3-30b-a3b", repo: "Qwen/Qwen3-30B-A3B-GGUF", file: "Qwen3-30B-A3B-Q4_K_M.gguf", bytes: 18556685824, sha256: "0d003f6662faee786ed5da3e31b29c978de5ae5d275c8794c606a7f3c01aa8f5", note: "a mixture-of-experts model (128 experts, 8 used) LARGER than 16 GiB of RAM: run it with `clutor generate ... --cache-mb 4096`" },
|
|
387
|
+
{ id: "qwen2.5-1.5b-q4_k_m", catalogId: "qwen2.5-1.5b", repo: "Qwen/Qwen2.5-1.5B-Instruct-GGUF", file: "qwen2.5-1.5b-instruct-q4_k_m.gguf", bytes: 1117320736, sha256: "6a1a2eb6d15622bf3c96857206351ba97e1af16c30d7a74ee38970e434e9407e", note: "a small dense model (1.0 GiB): the fourth architecture the engine can run with `clutor generate`" },
|
|
388
|
+
{ id: "qwen2.5-coder-7b-q4_k_m", catalogId: "qwen2.5-coder-7b", repo: "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF", file: "qwen2.5-coder-7b-instruct-q4_k_m.gguf", bytes: 4683073536, sha256: "509287f78cb4d4cf6b3843734733b914b2c158e43e22a7f4bf5e963800894d3c", note: "a dense coding model for the agent (not a mixture of experts)" }
|
|
385
389
|
];
|
|
386
390
|
var downloadUrl = (d) => `https://huggingface.co/${d.repo}/resolve/main/${d.file}`;
|
|
387
391
|
var DownloadError = class extends Error {
|
|
@@ -393,29 +397,49 @@ var DownloadError = class extends Error {
|
|
|
393
397
|
async function downloadFile(url, dest, opts = {}) {
|
|
394
398
|
const f = opts.fetch ?? globalThis.fetch;
|
|
395
399
|
await mkdir(resolve(dest, ".."), { recursive: true });
|
|
396
|
-
const
|
|
400
|
+
const part = `${dest}.part`;
|
|
401
|
+
const have = await stat(part).then((s) => s.size).catch(() => 0);
|
|
402
|
+
let res = await f(url, { redirect: "follow", ...have > 0 ? { headers: { Range: `bytes=${have}-` } } : {} });
|
|
403
|
+
if (res.status === 416) {
|
|
404
|
+
await rm(part, { force: true });
|
|
405
|
+
res = await f(url, { redirect: "follow" });
|
|
406
|
+
}
|
|
397
407
|
if (!res.ok || !res.body) throw new DownloadError(`the server answered ${res.status} for ${url}`);
|
|
398
|
-
const
|
|
408
|
+
const resumed = res.status === 206;
|
|
409
|
+
let start = 0, total;
|
|
410
|
+
if (resumed) {
|
|
411
|
+
const m = /^bytes (\d+)-\d+\/(\d+)$/.exec(res.headers.get("content-range") ?? "");
|
|
412
|
+
if (!m || Number(m[1]) !== have) throw new DownloadError("the server's answer to the resume request does not continue where the partial file stops");
|
|
413
|
+
start = have;
|
|
414
|
+
total = Number(m[2]);
|
|
415
|
+
} else {
|
|
416
|
+
total = Number(res.headers.get("content-length") ?? 0);
|
|
417
|
+
}
|
|
399
418
|
if (!total) throw new DownloadError("the server did not say how large the file is, so it cannot be checked");
|
|
400
419
|
if (opts.expectedBytes && Math.abs(total - opts.expectedBytes) / opts.expectedBytes > 0.05) throw new DownloadError(`expected about ${opts.expectedBytes} bytes but the server offers ${total}; not downloading`);
|
|
401
420
|
const fs = await statfs(resolve(dest, ".."));
|
|
402
421
|
const free = Number(fs.bavail) * Number(fs.bsize);
|
|
403
|
-
if (free < total * 1.1) throw new DownloadError(`not enough free disk: need ${(total / GiB_).toFixed(2)} GiB, have ${(free / GiB_).toFixed(2)} GiB`);
|
|
404
|
-
const
|
|
405
|
-
|
|
422
|
+
if (free < (total - start) * 1.1) throw new DownloadError(`not enough free disk: need ${((total - start) / GiB_).toFixed(2)} GiB, have ${(free / GiB_).toFixed(2)} GiB`);
|
|
423
|
+
const hash = opts.expectedSha256 ? createHash3("sha256") : void 0;
|
|
424
|
+
if (hash && start > 0) for await (const chunk of createReadStream(part)) hash.update(chunk);
|
|
425
|
+
let done = start;
|
|
406
426
|
const body = Readable.fromWeb(res.body);
|
|
407
427
|
body.on("data", (c) => {
|
|
408
428
|
done += c.length;
|
|
429
|
+
hash?.update(c);
|
|
409
430
|
opts.onProgress?.(done, total);
|
|
410
431
|
});
|
|
411
432
|
try {
|
|
412
|
-
await pipeline(body, createWriteStream(part));
|
|
413
|
-
if (done !== total) throw new DownloadError(`the download stopped at ${done} of ${total} bytes`);
|
|
414
|
-
await rename(part, dest);
|
|
433
|
+
await pipeline(body, createWriteStream(part, { flags: resumed ? "a" : "w" }));
|
|
415
434
|
} catch (e) {
|
|
435
|
+
throw new DownloadError(`the download was interrupted at ${done} of ${total} bytes (${e instanceof Error ? e.message : String(e)}); run the same command again to resume`);
|
|
436
|
+
}
|
|
437
|
+
if (done !== total) throw new DownloadError(`the download stopped at ${done} of ${total} bytes; run the same command again to resume`);
|
|
438
|
+
if (hash && hash.digest("hex") !== opts.expectedSha256?.toLowerCase()) {
|
|
416
439
|
await rm(part, { force: true });
|
|
417
|
-
throw
|
|
440
|
+
throw new DownloadError("the downloaded file does not match the SHA-256 the publisher lists; it was deleted");
|
|
418
441
|
}
|
|
442
|
+
await rename(part, dest);
|
|
419
443
|
return total;
|
|
420
444
|
}
|
|
421
445
|
|
|
@@ -539,7 +563,7 @@ async function handleMcp(req, ctx) {
|
|
|
539
563
|
if (req.jsonrpc !== "2.0" || typeof req.method !== "string") return err(-32600, "not a JSON-RPC 2.0 request");
|
|
540
564
|
switch (req.method) {
|
|
541
565
|
case "initialize":
|
|
542
|
-
return ok({ protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "clutor", version:
|
|
566
|
+
return ok({ protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "clutor", version: ctx.version ?? "unknown" } });
|
|
543
567
|
case "notifications/initialized":
|
|
544
568
|
return { status: 202, body: "" };
|
|
545
569
|
case "ping":
|
|
@@ -925,6 +949,7 @@ function createClutorServer(o) {
|
|
|
925
949
|
if (e instanceof DecideError) throw new HttpError(e.status, e.message);
|
|
926
950
|
throw e;
|
|
927
951
|
}
|
|
952
|
+
await Promise.all(ready);
|
|
928
953
|
const root = roots.values().next().value;
|
|
929
954
|
const receipt = root ? await sealDecision(root, model, b.prompt, b.choices, result, startedAt) : null;
|
|
930
955
|
return reply(200, { choice: result.choice, probabilities: result.probabilities, probabilityMassOnChoices: result.probabilityMassOnChoices, latencyMs: result.latencyMs, receipt });
|
|
@@ -1000,8 +1025,8 @@ function createClutorServer(o) {
|
|
|
1000
1025
|
}
|
|
1001
1026
|
} else {
|
|
1002
1027
|
const { mkdtemp, rm: rm2 } = await import("fs/promises");
|
|
1003
|
-
const { tmpdir:
|
|
1004
|
-
const dir = await mkdtemp(`${
|
|
1028
|
+
const { tmpdir: tmpdir3 } = await import("os");
|
|
1029
|
+
const dir = await mkdtemp(`${tmpdir3()}/clutor-bench-`);
|
|
1005
1030
|
cleanup = () => rm2(dir, { recursive: true, force: true });
|
|
1006
1031
|
file = `${dir}/bench.gguf`;
|
|
1007
1032
|
await writeSyntheticGguf(file, { arch: "qwen2moe", name: "bench-moe", layers: 8, embedding: 1024, heads: 16, kvHeads: 4, contextLength: 4096, expertCount: 32, expertsUsed: 4, expertFfn: 1024, denseFfn: 0, vocab: 4096, sharedExperts: 0 });
|
|
@@ -1022,7 +1047,7 @@ function createClutorServer(o) {
|
|
|
1022
1047
|
}
|
|
1023
1048
|
}
|
|
1024
1049
|
if (method === "POST" && path === "/api/mcp") {
|
|
1025
|
-
const r = await handleMcp(await readBody(req), { isRoot: (d) => rootFor(d).catch(() => null) });
|
|
1050
|
+
const r = await handleMcp(await readBody(req), { isRoot: (d) => rootFor(d).catch(() => null), version: o.version });
|
|
1026
1051
|
return r.status === 202 ? (res.writeHead(202, cors), void res.end()) : reply(r.status, r.body);
|
|
1027
1052
|
}
|
|
1028
1053
|
if (method === "POST" && path === "/api/agent/run") {
|
|
@@ -1134,8 +1159,9 @@ function createClutorServer(o) {
|
|
|
1134
1159
|
|
|
1135
1160
|
// src/engine.ts
|
|
1136
1161
|
import { spawn as spawn2 } from "child_process";
|
|
1137
|
-
import {
|
|
1138
|
-
import {
|
|
1162
|
+
import { createHash as createHash4 } from "crypto";
|
|
1163
|
+
import { accessSync as accessSync2, chmodSync, constants as constants2, copyFileSync, existsSync, mkdirSync as mkdirSync2, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
1164
|
+
import { homedir as homedir3, tmpdir } from "os";
|
|
1139
1165
|
import { delimiter as delimiter2, dirname, join as join5 } from "path";
|
|
1140
1166
|
import { fileURLToPath } from "url";
|
|
1141
1167
|
var EngineError = class extends Error {
|
|
@@ -1222,6 +1248,7 @@ function streamArgs(file, o) {
|
|
|
1222
1248
|
add("--window", o.window);
|
|
1223
1249
|
if (o.shift) add("--shift", `${o.shift.at},${o.shift.len}`);
|
|
1224
1250
|
if (o.noOsCache) a.push("--no-os-cache");
|
|
1251
|
+
if (o.trace) a.push("--trace", o.trace);
|
|
1225
1252
|
return a;
|
|
1226
1253
|
}
|
|
1227
1254
|
async function engineInfo(bin, file) {
|
|
@@ -1238,10 +1265,85 @@ async function engineStream(bin, file, o = {}, timeoutMs = 9e5) {
|
|
|
1238
1265
|
throw new EngineError("the engine printed something that is not a report");
|
|
1239
1266
|
}
|
|
1240
1267
|
}
|
|
1268
|
+
function engineRun(bin, args, onOut, timeoutMs = 36e5) {
|
|
1269
|
+
return new Promise((resolve3, reject) => {
|
|
1270
|
+
const child = spawn2(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1271
|
+
let stderr = "";
|
|
1272
|
+
const timer = setTimeout(() => {
|
|
1273
|
+
child.kill("SIGKILL");
|
|
1274
|
+
reject(new EngineError(`${bin} did not finish within ${timeoutMs / 1e3} seconds`));
|
|
1275
|
+
}, timeoutMs);
|
|
1276
|
+
child.stdout.setEncoding("utf8");
|
|
1277
|
+
child.stdout.on("data", onOut);
|
|
1278
|
+
child.stderr.on("data", (b) => {
|
|
1279
|
+
stderr += b.toString();
|
|
1280
|
+
});
|
|
1281
|
+
child.on("error", (e) => {
|
|
1282
|
+
clearTimeout(timer);
|
|
1283
|
+
reject(new EngineError(`could not start ${bin}: ${e.message}`));
|
|
1284
|
+
});
|
|
1285
|
+
child.on("close", (code2) => {
|
|
1286
|
+
clearTimeout(timer);
|
|
1287
|
+
resolve3({ code: code2 ?? 1, stderr });
|
|
1288
|
+
});
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
function enginePlatform() {
|
|
1292
|
+
const os = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : void 0;
|
|
1293
|
+
const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : void 0;
|
|
1294
|
+
return os && arch ? `${os}-${arch}` : void 0;
|
|
1295
|
+
}
|
|
1296
|
+
var RELEASE_BASE = (version2) => process.env["CLUTOR_RELEASE_BASE"] ?? `https://github.com/isagarpinninti/clutor/releases/download/v${version2}`;
|
|
1297
|
+
var MAX_ASSET = 64 * 2 ** 20;
|
|
1298
|
+
async function download(url) {
|
|
1299
|
+
if (!url.startsWith("https://") && !/^http:\/\/(127\.0\.0\.1|localhost)[:/]/.test(url)) throw new EngineError(`refusing to download over an insecure connection: ${url}`);
|
|
1300
|
+
let res;
|
|
1301
|
+
try {
|
|
1302
|
+
res = await fetch(url);
|
|
1303
|
+
} catch (e) {
|
|
1304
|
+
throw new EngineError(`could not reach ${url}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1305
|
+
}
|
|
1306
|
+
if (!res.ok) throw new EngineError(`${url} answered ${res.status}${res.status === 404 ? ": no prebuilt engine is published for this version and platform; use `clutor engine build` instead" : ""}`);
|
|
1307
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
1308
|
+
if (buf.length > MAX_ASSET) throw new EngineError(`${url} is larger than ${MAX_ASSET / 2 ** 20} MiB; refusing it`);
|
|
1309
|
+
return buf;
|
|
1310
|
+
}
|
|
1311
|
+
async function installPrebuilt(version2, dest = join5(homedir3(), ".clutor", "bin")) {
|
|
1312
|
+
const platform = enginePlatform();
|
|
1313
|
+
if (!platform) throw new EngineError(`no prebuilt engine is published for ${process.platform}/${process.arch}; use \`clutor engine build\` (needs a C compiler)`);
|
|
1314
|
+
const base = RELEASE_BASE(version2), asset = `clutor-engine-${platform}.tar.gz`;
|
|
1315
|
+
const sums = (await download(`${base}/SHA256SUMS`)).toString("utf8");
|
|
1316
|
+
const line = sums.split("\n").map((l) => l.trim().split(/\s+/)).find((p) => p.length >= 2 && p[1].replace(/^\*/, "") === asset);
|
|
1317
|
+
if (!line) throw new EngineError(`SHA256SUMS lists no ${asset}`);
|
|
1318
|
+
const tarball = await download(`${base}/${asset}`);
|
|
1319
|
+
const got = createHash4("sha256").update(tarball).digest("hex");
|
|
1320
|
+
if (got !== line[0].toLowerCase()) throw new EngineError(`checksum mismatch for ${asset}: expected ${line[0]}, got ${got}. Nothing was installed`);
|
|
1321
|
+
const work = mkdtempSync(join5(tmpdir(), "clutor-engine-install-"));
|
|
1322
|
+
try {
|
|
1323
|
+
const file = join5(work, asset);
|
|
1324
|
+
writeFileSync(file, tarball);
|
|
1325
|
+
const x = await run("tar", ["-xzf", file, "-C", work], { timeoutMs: 6e4 });
|
|
1326
|
+
if (x.code !== 0) throw new EngineError(`could not unpack ${asset}: ${x.stderr.trim()}`);
|
|
1327
|
+
const unpacked = join5(work, `clutor-engine-${platform}`, EXE);
|
|
1328
|
+
if (!existsSync(unpacked)) throw new EngineError(`${asset} does not contain ${EXE}`);
|
|
1329
|
+
chmodSync(unpacked, 493);
|
|
1330
|
+
const probe = await run(unpacked, ["--version"], { timeoutMs: 15e3 }).catch((e) => {
|
|
1331
|
+
throw new EngineError(`the downloaded engine does not run here: ${e instanceof Error ? e.message : String(e)}`);
|
|
1332
|
+
});
|
|
1333
|
+
if (probe.code !== 0) throw new EngineError(`the downloaded engine does not run here (exit ${probe.code}); use \`clutor engine build\``);
|
|
1334
|
+
mkdirSync2(dest, { recursive: true });
|
|
1335
|
+
const out = join5(dest, EXE);
|
|
1336
|
+
copyFileSync(unpacked, out);
|
|
1337
|
+
chmodSync(out, 493);
|
|
1338
|
+
return out;
|
|
1339
|
+
} finally {
|
|
1340
|
+
rmSync(work, { recursive: true, force: true });
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1241
1343
|
|
|
1242
1344
|
// src/cli.ts
|
|
1243
|
-
import { existsSync as existsSync2, mkdirSync as
|
|
1244
|
-
import { tmpdir } from "os";
|
|
1345
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1346
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1245
1347
|
import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
|
|
1246
1348
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1247
1349
|
import {
|
|
@@ -1292,9 +1394,13 @@ Usage: clutor <command> [options]
|
|
|
1292
1394
|
Replay a routing trace through the expert cache: baseline LRU against clutor
|
|
1293
1395
|
bench [--file model.gguf] [--engine] [--cache-mb N] [--tokens N] [--compute kernel|checksum]
|
|
1294
1396
|
Measure real disk reads on this machine; with the native engine, also compare cache and prefetch strategies
|
|
1295
|
-
stream <model.gguf> [--tokens N] [--cache-mb N] [--policy lru|wtinylfu|adaptive] [--smart J,M,TAU|off] [--prefetch on|off] [--dense auto|resident|ring] [--compute kernel|checksum] [--json]
|
|
1397
|
+
stream <model.gguf> [--tokens N] [--cache-mb N] [--policy lru|wtinylfu|adaptive] [--smart J,M,TAU|off] [--prefetch on|off] [--dense auto|resident|ring] [--compute kernel|checksum] [--trace FILE] [--json]
|
|
1296
1398
|
Stream a model's experts from disk through the native engine (synthetic routing; measured I/O and cache)
|
|
1297
|
-
|
|
1399
|
+
generate <model.gguf> "<prompt>" [--n N] [--cache-mb N] [--smart J,M,TAU] [--prefetch off|prev|ahead] [--trace FILE]
|
|
1400
|
+
Generate text with the native engine's real forward pass (olmoe models). With --cache-mb the experts stream from disk through a cache of that size
|
|
1401
|
+
quality <model.gguf> <text-file> --cache-mb N [--smart J,M,TAU] [--max-tokens N] [--json]
|
|
1402
|
+
Perplexity, KL divergence and top-token agreement of a streamed (optionally smart-route) run against exact routing
|
|
1403
|
+
engine install|build|where Install the prebuilt native engine (checked against a SHA-256), or compile it from c/ (needs a C compiler), or show which one is used
|
|
1298
1404
|
code "<task>" [--dir folder] [--apply] [--yes] [--verify "npm test"] [--max-steps N]
|
|
1299
1405
|
Run the coding agent on a folder; review the diff before anything is written
|
|
1300
1406
|
serve [dir] [--port ${DEFAULT_PORT}] [--folder dir]... Start the local server: OpenAI and Ollama compatible API, agent, MCP
|
|
@@ -1320,7 +1426,7 @@ async function main(argv, io, session) {
|
|
|
1320
1426
|
const json = flagBool(args, "json");
|
|
1321
1427
|
try {
|
|
1322
1428
|
if (flagBool(args, "version") || command === "version" || argv.includes("-v")) return io.out(version()), 0;
|
|
1323
|
-
if (!command && !flagBool(args, "help") && !argv.includes("-h") && io.prompt && !session) return await (await import("./repl-
|
|
1429
|
+
if (!command && !flagBool(args, "help") && !argv.includes("-h") && io.prompt && !session) return await (await import("./repl-DOWMDXFR.js")).runRepl(io, (a, s) => main(a, io, s), (cfg, what) => engineSlashCommand(io, cfg, what), version());
|
|
1324
1430
|
if (!command || flagBool(args, "help") || command === "help" || argv.includes("-h")) return io.out(HELP), 0;
|
|
1325
1431
|
switch (command) {
|
|
1326
1432
|
case "init":
|
|
@@ -1329,6 +1435,10 @@ async function main(argv, io, session) {
|
|
|
1329
1435
|
return await models(io, args, rest[0], json);
|
|
1330
1436
|
case "stream":
|
|
1331
1437
|
return await streamCmd(io, args, rest[0], json);
|
|
1438
|
+
case "generate":
|
|
1439
|
+
return await generateCmd(io, args, rest[0], rest.slice(1).join(" "));
|
|
1440
|
+
case "quality":
|
|
1441
|
+
return await qualityCmd(io, args, rest[0], rest[1], json);
|
|
1332
1442
|
case "engine":
|
|
1333
1443
|
return await engineCmd(io, rest[0]);
|
|
1334
1444
|
case "pull":
|
|
@@ -1377,13 +1487,13 @@ async function main(argv, io, session) {
|
|
|
1377
1487
|
}
|
|
1378
1488
|
function init(io, dirArg) {
|
|
1379
1489
|
const dir = resolve2(io.cwd, dirArg ?? ".");
|
|
1380
|
-
|
|
1490
|
+
mkdirSync3(join6(dir, "models"), { recursive: true });
|
|
1381
1491
|
const path = join6(dir, CONFIG_FILE);
|
|
1382
1492
|
if (existsSync2(path)) {
|
|
1383
1493
|
io.err(`${path} already exists; leaving it alone`);
|
|
1384
1494
|
return 1;
|
|
1385
1495
|
}
|
|
1386
|
-
|
|
1496
|
+
writeFileSync2(path, `${JSON.stringify({ port: DEFAULT_PORT, models: "models", backend: { kind: "none" }, memory: { contextTokens: 8192, kvBits: 16 }, cache: { policy: "adaptive" }, agent: { toolMode: "json", maxSteps: 30 } }, null, 2)}
|
|
1387
1497
|
`);
|
|
1388
1498
|
const h = hostInfo3();
|
|
1389
1499
|
io.out(`Created ${path} and ${join6(dir, "models")}.`);
|
|
@@ -1502,8 +1612,8 @@ async function bench(io, args, json) {
|
|
|
1502
1612
|
let cleanup;
|
|
1503
1613
|
if (file) path = resolve2(io.cwd, file);
|
|
1504
1614
|
else {
|
|
1505
|
-
const dir =
|
|
1506
|
-
cleanup = () =>
|
|
1615
|
+
const dir = mkdtempSync2(join6(tmpdir2(), "clutor-bench-"));
|
|
1616
|
+
cleanup = () => rmSync2(dir, { recursive: true, force: true });
|
|
1507
1617
|
path = join6(dir, "bench.gguf");
|
|
1508
1618
|
io.err("No --file given: writing a test model of about 1 GiB to the temp folder (it will be deleted).");
|
|
1509
1619
|
await writeSyntheticGguf2(path, { arch: "qwen2moe", name: "bench-moe", layers: 8, embedding: 1024, heads: 16, kvHeads: 4, contextLength: 4096, expertCount: 32, expertsUsed: 4, expertFfn: 1024, denseFfn: 0, vocab: 4096, sharedExperts: 0 });
|
|
@@ -1516,7 +1626,7 @@ async function bench(io, args, json) {
|
|
|
1516
1626
|
const m = await measureExpertReads2(path, layout, h.totalRamBytes, 96);
|
|
1517
1627
|
const dm = diskModelFrom(m, expertSizeBytes2(layout), layout.layers.find((l) => l.experts.length)?.experts[0]?.length ?? 1);
|
|
1518
1628
|
const bin = findEngine();
|
|
1519
|
-
if (flagBool(args, "engine") && !bin) throw new UsageError("the native engine is not
|
|
1629
|
+
if (flagBool(args, "engine") && !bin) throw new UsageError("the native engine is not installed: run `clutor engine install` (prebuilt) or `clutor engine build` (needs a C compiler)");
|
|
1520
1630
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1521
1631
|
const cmp = bin ? await compareStrategies(bin, path, flagInt(args, "cache-mb", 1, 1e6) ?? Math.max(64, Math.floor(h.totalRamBytes / 2 ** 20 / 16)), flagInt(args, "tokens", 10, 1e5) ?? 300, flagString(args, "dense"), computeFlag(args)) : void 0;
|
|
1522
1632
|
if (json) {
|
|
@@ -1958,7 +2068,7 @@ async function pull(io, args, id) {
|
|
|
1958
2068
|
}
|
|
1959
2069
|
}
|
|
1960
2070
|
let last = 0;
|
|
1961
|
-
const bytes = await downloadFile(downloadUrl(d), dest, { expectedBytes: d.bytes, onProgress: (done, total) => {
|
|
2071
|
+
const bytes = await downloadFile(downloadUrl(d), dest, { expectedBytes: d.bytes, expectedSha256: d.sha256, onProgress: (done, total) => {
|
|
1962
2072
|
const p = Math.floor(done / total * 10);
|
|
1963
2073
|
if (p > last) {
|
|
1964
2074
|
last = p;
|
|
@@ -2017,10 +2127,91 @@ function formatComparison(c) {
|
|
|
2017
2127
|
table(rows, ["", ...c.map((x) => x.name)])
|
|
2018
2128
|
].join("\n");
|
|
2019
2129
|
}
|
|
2130
|
+
function engineFlags(args) {
|
|
2131
|
+
const out = [];
|
|
2132
|
+
const num = (n, lo, hi) => {
|
|
2133
|
+
const v = flagInt(args, n, lo, hi);
|
|
2134
|
+
if (v !== void 0) out.push(`--${n}`, String(v));
|
|
2135
|
+
};
|
|
2136
|
+
num("cache-mb", 1, 1e7);
|
|
2137
|
+
num("io-threads", 1, 16);
|
|
2138
|
+
num("prefetch-n", 1, 64);
|
|
2139
|
+
num("stage", 1, 1e5);
|
|
2140
|
+
const pol = flagString(args, "policy");
|
|
2141
|
+
if (pol) {
|
|
2142
|
+
if (!["lru", "wtinylfu", "adaptive"].includes(pol)) throw new UsageError("--policy must be lru, wtinylfu or adaptive");
|
|
2143
|
+
out.push("--policy", pol);
|
|
2144
|
+
}
|
|
2145
|
+
const pf = flagString(args, "prefetch");
|
|
2146
|
+
if (pf) {
|
|
2147
|
+
if (!["on", "off", "prev", "ahead"].includes(pf)) throw new UsageError("--prefetch must be off, prev or ahead");
|
|
2148
|
+
out.push("--prefetch", pf);
|
|
2149
|
+
}
|
|
2150
|
+
const sm = flagString(args, "smart");
|
|
2151
|
+
if (sm && sm !== "off") {
|
|
2152
|
+
if (!/^\d+,\d+,[\d.]+$/.test(sm)) throw new UsageError("--smart wants J,M,TAU, for example 4,16,0.02");
|
|
2153
|
+
out.push("--smart", sm);
|
|
2154
|
+
}
|
|
2155
|
+
if (flagBool(args, "no-os-cache")) out.push("--no-os-cache");
|
|
2156
|
+
return out;
|
|
2157
|
+
}
|
|
2158
|
+
async function generateCmd(io, args, file, prompt) {
|
|
2159
|
+
if (!file || !prompt.trim()) throw new UsageError('usage: clutor generate <model.gguf> "<prompt>" [--n N] [--cache-mb N] [--smart J,M,TAU]');
|
|
2160
|
+
const bin = findEngine();
|
|
2161
|
+
if (!bin) throw new UsageError("the native engine is not installed: run `clutor engine install` (prebuilt) or `clutor engine build` (needs a C compiler)");
|
|
2162
|
+
const path = resolve2(io.cwd, file);
|
|
2163
|
+
if (!existsSync2(path)) throw new UsageError(`${path} does not exist`);
|
|
2164
|
+
const n = flagInt(args, "n", 1, 1e5) ?? 64;
|
|
2165
|
+
const trace = flagString(args, "trace");
|
|
2166
|
+
const argv = ["generate", path, prompt, "--n", String(n), ...engineFlags(args), ...trace ? ["--trace", resolve2(io.cwd, trace)] : []];
|
|
2167
|
+
const r = await engineRun(bin, argv, (s) => (io.write ?? ((t) => io.out(t.replace(/\n$/, ""))))(s));
|
|
2168
|
+
io.write?.("\n");
|
|
2169
|
+
if (r.stderr.trim()) io.err(r.stderr.trim());
|
|
2170
|
+
if (r.code !== 0) {
|
|
2171
|
+
io.err(`the engine exited with ${r.code}`);
|
|
2172
|
+
return 1;
|
|
2173
|
+
}
|
|
2174
|
+
return 0;
|
|
2175
|
+
}
|
|
2176
|
+
async function qualityCmd(io, args, file, text, json) {
|
|
2177
|
+
if (!file || !text) throw new UsageError("usage: clutor quality <model.gguf> <text-file> --cache-mb N [--smart J,M,TAU] [--max-tokens N]");
|
|
2178
|
+
if (flagInt(args, "cache-mb", 1, 1e7) === void 0) throw new UsageError("quality compares against a streamed run: give --cache-mb N");
|
|
2179
|
+
const bin = findEngine();
|
|
2180
|
+
if (!bin) throw new UsageError("the native engine is not installed: run `clutor engine install` (prebuilt) or `clutor engine build` (needs a C compiler)");
|
|
2181
|
+
const path = resolve2(io.cwd, file), textPath = resolve2(io.cwd, text);
|
|
2182
|
+
if (!existsSync2(path)) throw new UsageError(`${path} does not exist`);
|
|
2183
|
+
if (!existsSync2(textPath)) throw new UsageError(`${textPath} does not exist`);
|
|
2184
|
+
const mt = flagInt(args, "max-tokens", 2, 1e6);
|
|
2185
|
+
const r = await run(bin, ["quality", path, textPath, ...engineFlags(args), ...mt ? ["--max-tokens", String(mt)] : []], { timeoutMs: 36e5 });
|
|
2186
|
+
if (r.code !== 0) {
|
|
2187
|
+
io.err(r.stderr.trim() || `the engine exited with ${r.code}`);
|
|
2188
|
+
return 1;
|
|
2189
|
+
}
|
|
2190
|
+
if (json) {
|
|
2191
|
+
io.out(r.stdout.trim());
|
|
2192
|
+
return 0;
|
|
2193
|
+
}
|
|
2194
|
+
let q;
|
|
2195
|
+
try {
|
|
2196
|
+
q = JSON.parse(r.stdout);
|
|
2197
|
+
} catch {
|
|
2198
|
+
throw new EngineError("the engine printed something that is not a report");
|
|
2199
|
+
}
|
|
2200
|
+
const n = (k) => q[k];
|
|
2201
|
+
io.out([
|
|
2202
|
+
`MEASURED ${n("tokensScored")} tokens scored, teacher-forced`,
|
|
2203
|
+
`MEASURED perplexity ${n("perplexityExact").toFixed(3)} exact routing, ${n("perplexityRun").toFixed(3)} this run (x${n("perplexityRatio").toFixed(4)})`,
|
|
2204
|
+
`MEASURED KL divergence from exact ${n("meanKL").toFixed(4)}; the top token is unchanged at ${(n("top1Agreement") * 100).toFixed(1)}% of positions`,
|
|
2205
|
+
q["smart"] ? `MEASURED smart-route used a resident expert for ${(100 * n("substitutions") / n("expertSlots")).toFixed(1)}% of expert slots; the largest probability moved was ${n("maxProbMoved").toFixed(4)}` : "smart-route off",
|
|
2206
|
+
`MEASURED expert cache hit rate ${(n("hitRate") * 100).toFixed(1)}%, ${n("gbPerToken").toFixed(3)} GB read per token`,
|
|
2207
|
+
"The exact reference maps the whole file, so the OS cache is warm: time is not reported here."
|
|
2208
|
+
].join("\n"));
|
|
2209
|
+
return 0;
|
|
2210
|
+
}
|
|
2020
2211
|
async function streamCmd(io, args, file, json) {
|
|
2021
2212
|
if (!file) throw new UsageError("usage: clutor stream <model.gguf> [--tokens N] [--cache-mb N] [--policy lru|wtinylfu|adaptive] [--smart J,M,TAU|off] [--prefetch on|off]");
|
|
2022
2213
|
const bin = findEngine();
|
|
2023
|
-
if (!bin) throw new UsageError("the native engine is not
|
|
2214
|
+
if (!bin) throw new UsageError("the native engine is not installed: run `clutor engine install` (prebuilt) or `clutor engine build` (needs a C compiler)");
|
|
2024
2215
|
const num = (n, lo, hi) => flagInt(args, n, lo, hi);
|
|
2025
2216
|
const pol = flagString(args, "policy");
|
|
2026
2217
|
if (pol && !["lru", "wtinylfu", "adaptive"].includes(pol)) throw new UsageError("--policy must be lru, wtinylfu or adaptive");
|
|
@@ -2040,6 +2231,7 @@ async function streamCmd(io, args, file, json) {
|
|
|
2040
2231
|
...dn ? { dense: dn } : {},
|
|
2041
2232
|
...computeFlag(args) ? { compute: computeFlag(args) } : {},
|
|
2042
2233
|
...flagBool(args, "no-os-cache") ? { noOsCache: true } : {},
|
|
2234
|
+
...flagString(args, "trace") ? { trace: flagString(args, "trace") } : {},
|
|
2043
2235
|
...num("window", 1, 1e5) !== void 0 ? { window: num("window", 1, 1e5) } : {},
|
|
2044
2236
|
...shiftFlag(args)
|
|
2045
2237
|
};
|
|
@@ -2069,7 +2261,7 @@ function describeStream(r) {
|
|
|
2069
2261
|
lines.push(`MEASURED hit rate and peak RSS per window of tokens${note}:
|
|
2070
2262
|
${table(r.windows.map((w, i) => [`${i ? r.windows[i - 1].tokEnd + 1 : 1}-${w.tokEnd}`, pct(w.accesses ? w.hits / w.accesses : 0), `${(w.peakRssBytes / 2 ** 20).toFixed(0)} MiB`]), ["tokens", "hit rate", "peak RSS"])}`);
|
|
2071
2263
|
}
|
|
2072
|
-
lines.push(`${t.tokensPerSecond.toFixed(1)} passes per second over SYNTHETIC routing; the compute is ${r.config.computeMode === "kernel" ? `a Q4_0 x Q8_0 kernel (${r.config.computeKernel}) over the loaded bytes (${t.computeSeconds.toFixed(2)} s in total), not this model's maths` : "a checksum"}: this is not model tokens per second.`);
|
|
2264
|
+
lines.push(`${t.tokensPerSecond.toFixed(1)} passes per second over ${r.provenance.routing === "real" ? "REAL (replayed) routing recorded from this model" : "SYNTHETIC routing"}; the compute is ${r.config.computeMode === "kernel" ? `a Q4_0 x Q8_0 kernel (${r.config.computeKernel}) over the loaded bytes (${t.computeSeconds.toFixed(2)} s in total), not this model's maths` : "a checksum"}: this is not model tokens per second.`);
|
|
2073
2265
|
return lines.join("\n");
|
|
2074
2266
|
}
|
|
2075
2267
|
async function engineCmd(io, sub) {
|
|
@@ -2091,7 +2283,20 @@ async function engineCmd(io, sub) {
|
|
|
2091
2283
|
throw e;
|
|
2092
2284
|
}
|
|
2093
2285
|
}
|
|
2094
|
-
|
|
2286
|
+
if (sub === "install") {
|
|
2287
|
+
io.err("Downloading the prebuilt native engine and checking its SHA-256...");
|
|
2288
|
+
try {
|
|
2289
|
+
io.out(`Installed ${await installPrebuilt(version())}`);
|
|
2290
|
+
return 0;
|
|
2291
|
+
} catch (e) {
|
|
2292
|
+
if (e instanceof EngineError) {
|
|
2293
|
+
io.err(e.message);
|
|
2294
|
+
return 1;
|
|
2295
|
+
}
|
|
2296
|
+
throw e;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
throw new UsageError("usage: clutor engine install | build | where");
|
|
2095
2300
|
}
|
|
2096
2301
|
async function engineSlashCommand(io, config, what) {
|
|
2097
2302
|
const file = config.backend.llamaServer.model;
|
|
@@ -2160,8 +2365,11 @@ export {
|
|
|
2160
2365
|
buildEngine,
|
|
2161
2366
|
engineInfo,
|
|
2162
2367
|
engineStream,
|
|
2368
|
+
engineRun,
|
|
2369
|
+
enginePlatform,
|
|
2370
|
+
installPrebuilt,
|
|
2163
2371
|
version,
|
|
2164
2372
|
main,
|
|
2165
2373
|
startBackendIfConfigured
|
|
2166
2374
|
};
|
|
2167
|
-
//# sourceMappingURL=chunk-
|
|
2375
|
+
//# sourceMappingURL=chunk-ODS6ND7T.js.map
|