@clutor/cli 0.1.0 → 0.2.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 +85 -0
- package/c/src/cache.c +232 -0
- package/c/src/cache.h +48 -0
- package/c/src/forward.c +354 -0
- package/c/src/forward.h +69 -0
- package/c/src/gguf.c +242 -0
- package/c/src/gguf.h +48 -0
- package/c/src/kernel.c +125 -0
- package/c/src/kernel.h +45 -0
- package/c/src/main.c +476 -0
- 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/route.c +52 -0
- package/c/src/route.h +27 -0
- package/c/src/stream.c +399 -0
- package/c/src/stream.h +68 -0
- package/c/src/tokenizer.c +287 -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-6WAEQCCP.js → chunk-2FBPYNHP.js} +249 -40
- package/dist/chunk-2FBPYNHP.js.map +1 -0
- package/dist/{chunk-RBSFKJ6L.js → chunk-BZZUO7XX.js} +35 -6
- package/dist/chunk-BZZUO7XX.js.map +1 -0
- package/dist/index.d.ts +27 -8
- package/dist/index.js +8 -2
- package/dist/{repl-OVODHQI5.js → repl-DOWMDXFR.js} +2 -2
- package/package.json +4 -4
- package/dist/chunk-6WAEQCCP.js.map +0 -1
- package/dist/chunk-RBSFKJ6L.js.map +0 -1
- /package/dist/{repl-OVODHQI5.js.map → repl-DOWMDXFR.js.map} +0 -0
|
@@ -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,9 @@ 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: "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`" },
|
|
386
|
+
{ 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
387
|
];
|
|
386
388
|
var downloadUrl = (d) => `https://huggingface.co/${d.repo}/resolve/main/${d.file}`;
|
|
387
389
|
var DownloadError = class extends Error {
|
|
@@ -393,29 +395,49 @@ var DownloadError = class extends Error {
|
|
|
393
395
|
async function downloadFile(url, dest, opts = {}) {
|
|
394
396
|
const f = opts.fetch ?? globalThis.fetch;
|
|
395
397
|
await mkdir(resolve(dest, ".."), { recursive: true });
|
|
396
|
-
const
|
|
398
|
+
const part = `${dest}.part`;
|
|
399
|
+
const have = await stat(part).then((s) => s.size).catch(() => 0);
|
|
400
|
+
let res = await f(url, { redirect: "follow", ...have > 0 ? { headers: { Range: `bytes=${have}-` } } : {} });
|
|
401
|
+
if (res.status === 416) {
|
|
402
|
+
await rm(part, { force: true });
|
|
403
|
+
res = await f(url, { redirect: "follow" });
|
|
404
|
+
}
|
|
397
405
|
if (!res.ok || !res.body) throw new DownloadError(`the server answered ${res.status} for ${url}`);
|
|
398
|
-
const
|
|
406
|
+
const resumed = res.status === 206;
|
|
407
|
+
let start = 0, total;
|
|
408
|
+
if (resumed) {
|
|
409
|
+
const m = /^bytes (\d+)-\d+\/(\d+)$/.exec(res.headers.get("content-range") ?? "");
|
|
410
|
+
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");
|
|
411
|
+
start = have;
|
|
412
|
+
total = Number(m[2]);
|
|
413
|
+
} else {
|
|
414
|
+
total = Number(res.headers.get("content-length") ?? 0);
|
|
415
|
+
}
|
|
399
416
|
if (!total) throw new DownloadError("the server did not say how large the file is, so it cannot be checked");
|
|
400
417
|
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
418
|
const fs = await statfs(resolve(dest, ".."));
|
|
402
419
|
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
|
-
|
|
420
|
+
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`);
|
|
421
|
+
const hash = opts.expectedSha256 ? createHash3("sha256") : void 0;
|
|
422
|
+
if (hash && start > 0) for await (const chunk of createReadStream(part)) hash.update(chunk);
|
|
423
|
+
let done = start;
|
|
406
424
|
const body = Readable.fromWeb(res.body);
|
|
407
425
|
body.on("data", (c) => {
|
|
408
426
|
done += c.length;
|
|
427
|
+
hash?.update(c);
|
|
409
428
|
opts.onProgress?.(done, total);
|
|
410
429
|
});
|
|
411
430
|
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);
|
|
431
|
+
await pipeline(body, createWriteStream(part, { flags: resumed ? "a" : "w" }));
|
|
415
432
|
} catch (e) {
|
|
433
|
+
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`);
|
|
434
|
+
}
|
|
435
|
+
if (done !== total) throw new DownloadError(`the download stopped at ${done} of ${total} bytes; run the same command again to resume`);
|
|
436
|
+
if (hash && hash.digest("hex") !== opts.expectedSha256?.toLowerCase()) {
|
|
416
437
|
await rm(part, { force: true });
|
|
417
|
-
throw
|
|
438
|
+
throw new DownloadError("the downloaded file does not match the SHA-256 the publisher lists; it was deleted");
|
|
418
439
|
}
|
|
440
|
+
await rename(part, dest);
|
|
419
441
|
return total;
|
|
420
442
|
}
|
|
421
443
|
|
|
@@ -539,7 +561,7 @@ async function handleMcp(req, ctx) {
|
|
|
539
561
|
if (req.jsonrpc !== "2.0" || typeof req.method !== "string") return err(-32600, "not a JSON-RPC 2.0 request");
|
|
540
562
|
switch (req.method) {
|
|
541
563
|
case "initialize":
|
|
542
|
-
return ok({ protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "clutor", version:
|
|
564
|
+
return ok({ protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "clutor", version: ctx.version ?? "unknown" } });
|
|
543
565
|
case "notifications/initialized":
|
|
544
566
|
return { status: 202, body: "" };
|
|
545
567
|
case "ping":
|
|
@@ -1000,8 +1022,8 @@ function createClutorServer(o) {
|
|
|
1000
1022
|
}
|
|
1001
1023
|
} else {
|
|
1002
1024
|
const { mkdtemp, rm: rm2 } = await import("fs/promises");
|
|
1003
|
-
const { tmpdir:
|
|
1004
|
-
const dir = await mkdtemp(`${
|
|
1025
|
+
const { tmpdir: tmpdir3 } = await import("os");
|
|
1026
|
+
const dir = await mkdtemp(`${tmpdir3()}/clutor-bench-`);
|
|
1005
1027
|
cleanup = () => rm2(dir, { recursive: true, force: true });
|
|
1006
1028
|
file = `${dir}/bench.gguf`;
|
|
1007
1029
|
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 +1044,7 @@ function createClutorServer(o) {
|
|
|
1022
1044
|
}
|
|
1023
1045
|
}
|
|
1024
1046
|
if (method === "POST" && path === "/api/mcp") {
|
|
1025
|
-
const r = await handleMcp(await readBody(req), { isRoot: (d) => rootFor(d).catch(() => null) });
|
|
1047
|
+
const r = await handleMcp(await readBody(req), { isRoot: (d) => rootFor(d).catch(() => null), version: o.version });
|
|
1026
1048
|
return r.status === 202 ? (res.writeHead(202, cors), void res.end()) : reply(r.status, r.body);
|
|
1027
1049
|
}
|
|
1028
1050
|
if (method === "POST" && path === "/api/agent/run") {
|
|
@@ -1134,8 +1156,9 @@ function createClutorServer(o) {
|
|
|
1134
1156
|
|
|
1135
1157
|
// src/engine.ts
|
|
1136
1158
|
import { spawn as spawn2 } from "child_process";
|
|
1137
|
-
import {
|
|
1138
|
-
import {
|
|
1159
|
+
import { createHash as createHash4 } from "crypto";
|
|
1160
|
+
import { accessSync as accessSync2, chmodSync, constants as constants2, copyFileSync, existsSync, mkdirSync as mkdirSync2, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
1161
|
+
import { homedir as homedir3, tmpdir } from "os";
|
|
1139
1162
|
import { delimiter as delimiter2, dirname, join as join5 } from "path";
|
|
1140
1163
|
import { fileURLToPath } from "url";
|
|
1141
1164
|
var EngineError = class extends Error {
|
|
@@ -1154,8 +1177,11 @@ var executable = (p) => {
|
|
|
1154
1177
|
}
|
|
1155
1178
|
};
|
|
1156
1179
|
function engineSourceDir() {
|
|
1157
|
-
|
|
1180
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1181
|
+
const packaged = join5(here, "..", "c");
|
|
1182
|
+
return existsSync(join5(packaged, "Makefile")) ? packaged : join5(here, "..", "..", "..", "c");
|
|
1158
1183
|
}
|
|
1184
|
+
var isPackaged = () => existsSync(join5(dirname(fileURLToPath(import.meta.url)), "..", "c", "Makefile"));
|
|
1159
1185
|
function findEngine(env = process.env) {
|
|
1160
1186
|
const candidates = [env["CLUTOR_ENGINE"], join5(engineSourceDir(), "build", EXE), join5(homedir3(), ".clutor", "bin", EXE), ...(env["PATH"] ?? "").split(delimiter2).map((d) => join5(d, EXE))];
|
|
1161
1187
|
return candidates.find((c) => !!c && executable(c));
|
|
@@ -1186,12 +1212,13 @@ function run(bin, args, opts = {}) {
|
|
|
1186
1212
|
}
|
|
1187
1213
|
async function buildEngine() {
|
|
1188
1214
|
const dir = engineSourceDir();
|
|
1189
|
-
const
|
|
1215
|
+
const out = isPackaged() ? join5(homedir3(), ".clutor", "bin") : join5(dir, "build");
|
|
1216
|
+
const r = await run("make", ["-C", dir, ...isPackaged() ? [`BUILD=${out}`] : []], { timeoutMs: 3e5 }).catch((e) => {
|
|
1190
1217
|
throw new EngineError(`could not run make: ${e instanceof Error ? e.message : String(e)}`);
|
|
1191
1218
|
});
|
|
1192
1219
|
if (r.code !== 0) throw new EngineError(`the engine did not compile:
|
|
1193
1220
|
${(r.stderr || r.stdout).split("\n").slice(-15).join("\n")}`);
|
|
1194
|
-
const bin = join5(
|
|
1221
|
+
const bin = join5(out, EXE);
|
|
1195
1222
|
if (!executable(bin)) throw new EngineError(`make finished but ${bin} is missing`);
|
|
1196
1223
|
return bin;
|
|
1197
1224
|
}
|
|
@@ -1218,6 +1245,7 @@ function streamArgs(file, o) {
|
|
|
1218
1245
|
add("--window", o.window);
|
|
1219
1246
|
if (o.shift) add("--shift", `${o.shift.at},${o.shift.len}`);
|
|
1220
1247
|
if (o.noOsCache) a.push("--no-os-cache");
|
|
1248
|
+
if (o.trace) a.push("--trace", o.trace);
|
|
1221
1249
|
return a;
|
|
1222
1250
|
}
|
|
1223
1251
|
async function engineInfo(bin, file) {
|
|
@@ -1234,10 +1262,85 @@ async function engineStream(bin, file, o = {}, timeoutMs = 9e5) {
|
|
|
1234
1262
|
throw new EngineError("the engine printed something that is not a report");
|
|
1235
1263
|
}
|
|
1236
1264
|
}
|
|
1265
|
+
function engineRun(bin, args, onOut, timeoutMs = 36e5) {
|
|
1266
|
+
return new Promise((resolve3, reject) => {
|
|
1267
|
+
const child = spawn2(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1268
|
+
let stderr = "";
|
|
1269
|
+
const timer = setTimeout(() => {
|
|
1270
|
+
child.kill("SIGKILL");
|
|
1271
|
+
reject(new EngineError(`${bin} did not finish within ${timeoutMs / 1e3} seconds`));
|
|
1272
|
+
}, timeoutMs);
|
|
1273
|
+
child.stdout.setEncoding("utf8");
|
|
1274
|
+
child.stdout.on("data", onOut);
|
|
1275
|
+
child.stderr.on("data", (b) => {
|
|
1276
|
+
stderr += b.toString();
|
|
1277
|
+
});
|
|
1278
|
+
child.on("error", (e) => {
|
|
1279
|
+
clearTimeout(timer);
|
|
1280
|
+
reject(new EngineError(`could not start ${bin}: ${e.message}`));
|
|
1281
|
+
});
|
|
1282
|
+
child.on("close", (code2) => {
|
|
1283
|
+
clearTimeout(timer);
|
|
1284
|
+
resolve3({ code: code2 ?? 1, stderr });
|
|
1285
|
+
});
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
function enginePlatform() {
|
|
1289
|
+
const os = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : void 0;
|
|
1290
|
+
const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : void 0;
|
|
1291
|
+
return os && arch ? `${os}-${arch}` : void 0;
|
|
1292
|
+
}
|
|
1293
|
+
var RELEASE_BASE = (version2) => process.env["CLUTOR_RELEASE_BASE"] ?? `https://github.com/isagarpinninti/clutor/releases/download/v${version2}`;
|
|
1294
|
+
var MAX_ASSET = 64 * 2 ** 20;
|
|
1295
|
+
async function download(url) {
|
|
1296
|
+
if (!url.startsWith("https://") && !/^http:\/\/(127\.0\.0\.1|localhost)[:/]/.test(url)) throw new EngineError(`refusing to download over an insecure connection: ${url}`);
|
|
1297
|
+
let res;
|
|
1298
|
+
try {
|
|
1299
|
+
res = await fetch(url);
|
|
1300
|
+
} catch (e) {
|
|
1301
|
+
throw new EngineError(`could not reach ${url}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1302
|
+
}
|
|
1303
|
+
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" : ""}`);
|
|
1304
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
1305
|
+
if (buf.length > MAX_ASSET) throw new EngineError(`${url} is larger than ${MAX_ASSET / 2 ** 20} MiB; refusing it`);
|
|
1306
|
+
return buf;
|
|
1307
|
+
}
|
|
1308
|
+
async function installPrebuilt(version2, dest = join5(homedir3(), ".clutor", "bin")) {
|
|
1309
|
+
const platform = enginePlatform();
|
|
1310
|
+
if (!platform) throw new EngineError(`no prebuilt engine is published for ${process.platform}/${process.arch}; use \`clutor engine build\` (needs a C compiler)`);
|
|
1311
|
+
const base = RELEASE_BASE(version2), asset = `clutor-engine-${platform}.tar.gz`;
|
|
1312
|
+
const sums = (await download(`${base}/SHA256SUMS`)).toString("utf8");
|
|
1313
|
+
const line = sums.split("\n").map((l) => l.trim().split(/\s+/)).find((p) => p.length >= 2 && p[1].replace(/^\*/, "") === asset);
|
|
1314
|
+
if (!line) throw new EngineError(`SHA256SUMS lists no ${asset}`);
|
|
1315
|
+
const tarball = await download(`${base}/${asset}`);
|
|
1316
|
+
const got = createHash4("sha256").update(tarball).digest("hex");
|
|
1317
|
+
if (got !== line[0].toLowerCase()) throw new EngineError(`checksum mismatch for ${asset}: expected ${line[0]}, got ${got}. Nothing was installed`);
|
|
1318
|
+
const work = mkdtempSync(join5(tmpdir(), "clutor-engine-install-"));
|
|
1319
|
+
try {
|
|
1320
|
+
const file = join5(work, asset);
|
|
1321
|
+
writeFileSync(file, tarball);
|
|
1322
|
+
const x = await run("tar", ["-xzf", file, "-C", work], { timeoutMs: 6e4 });
|
|
1323
|
+
if (x.code !== 0) throw new EngineError(`could not unpack ${asset}: ${x.stderr.trim()}`);
|
|
1324
|
+
const unpacked = join5(work, `clutor-engine-${platform}`, EXE);
|
|
1325
|
+
if (!existsSync(unpacked)) throw new EngineError(`${asset} does not contain ${EXE}`);
|
|
1326
|
+
chmodSync(unpacked, 493);
|
|
1327
|
+
const probe = await run(unpacked, ["--version"], { timeoutMs: 15e3 }).catch((e) => {
|
|
1328
|
+
throw new EngineError(`the downloaded engine does not run here: ${e instanceof Error ? e.message : String(e)}`);
|
|
1329
|
+
});
|
|
1330
|
+
if (probe.code !== 0) throw new EngineError(`the downloaded engine does not run here (exit ${probe.code}); use \`clutor engine build\``);
|
|
1331
|
+
mkdirSync2(dest, { recursive: true });
|
|
1332
|
+
const out = join5(dest, EXE);
|
|
1333
|
+
copyFileSync(unpacked, out);
|
|
1334
|
+
chmodSync(out, 493);
|
|
1335
|
+
return out;
|
|
1336
|
+
} finally {
|
|
1337
|
+
rmSync(work, { recursive: true, force: true });
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1237
1340
|
|
|
1238
1341
|
// src/cli.ts
|
|
1239
|
-
import { existsSync, mkdirSync as
|
|
1240
|
-
import { tmpdir } from "os";
|
|
1342
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1343
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1241
1344
|
import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
|
|
1242
1345
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1243
1346
|
import {
|
|
@@ -1288,9 +1391,13 @@ Usage: clutor <command> [options]
|
|
|
1288
1391
|
Replay a routing trace through the expert cache: baseline LRU against clutor
|
|
1289
1392
|
bench [--file model.gguf] [--engine] [--cache-mb N] [--tokens N] [--compute kernel|checksum]
|
|
1290
1393
|
Measure real disk reads on this machine; with the native engine, also compare cache and prefetch strategies
|
|
1291
|
-
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]
|
|
1394
|
+
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]
|
|
1292
1395
|
Stream a model's experts from disk through the native engine (synthetic routing; measured I/O and cache)
|
|
1293
|
-
|
|
1396
|
+
generate <model.gguf> "<prompt>" [--n N] [--cache-mb N] [--smart J,M,TAU] [--prefetch off|prev|ahead] [--trace FILE]
|
|
1397
|
+
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
|
|
1398
|
+
quality <model.gguf> <text-file> --cache-mb N [--smart J,M,TAU] [--max-tokens N] [--json]
|
|
1399
|
+
Perplexity, KL divergence and top-token agreement of a streamed (optionally smart-route) run against exact routing
|
|
1400
|
+
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
|
|
1294
1401
|
code "<task>" [--dir folder] [--apply] [--yes] [--verify "npm test"] [--max-steps N]
|
|
1295
1402
|
Run the coding agent on a folder; review the diff before anything is written
|
|
1296
1403
|
serve [dir] [--port ${DEFAULT_PORT}] [--folder dir]... Start the local server: OpenAI and Ollama compatible API, agent, MCP
|
|
@@ -1316,7 +1423,7 @@ async function main(argv, io, session) {
|
|
|
1316
1423
|
const json = flagBool(args, "json");
|
|
1317
1424
|
try {
|
|
1318
1425
|
if (flagBool(args, "version") || command === "version" || argv.includes("-v")) return io.out(version()), 0;
|
|
1319
|
-
if (!command && !flagBool(args, "help") && !argv.includes("-h") && io.prompt && !session) return await (await import("./repl-
|
|
1426
|
+
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());
|
|
1320
1427
|
if (!command || flagBool(args, "help") || command === "help" || argv.includes("-h")) return io.out(HELP), 0;
|
|
1321
1428
|
switch (command) {
|
|
1322
1429
|
case "init":
|
|
@@ -1325,6 +1432,10 @@ async function main(argv, io, session) {
|
|
|
1325
1432
|
return await models(io, args, rest[0], json);
|
|
1326
1433
|
case "stream":
|
|
1327
1434
|
return await streamCmd(io, args, rest[0], json);
|
|
1435
|
+
case "generate":
|
|
1436
|
+
return await generateCmd(io, args, rest[0], rest.slice(1).join(" "));
|
|
1437
|
+
case "quality":
|
|
1438
|
+
return await qualityCmd(io, args, rest[0], rest[1], json);
|
|
1328
1439
|
case "engine":
|
|
1329
1440
|
return await engineCmd(io, rest[0]);
|
|
1330
1441
|
case "pull":
|
|
@@ -1373,13 +1484,13 @@ async function main(argv, io, session) {
|
|
|
1373
1484
|
}
|
|
1374
1485
|
function init(io, dirArg) {
|
|
1375
1486
|
const dir = resolve2(io.cwd, dirArg ?? ".");
|
|
1376
|
-
|
|
1487
|
+
mkdirSync3(join6(dir, "models"), { recursive: true });
|
|
1377
1488
|
const path = join6(dir, CONFIG_FILE);
|
|
1378
|
-
if (
|
|
1489
|
+
if (existsSync2(path)) {
|
|
1379
1490
|
io.err(`${path} already exists; leaving it alone`);
|
|
1380
1491
|
return 1;
|
|
1381
1492
|
}
|
|
1382
|
-
|
|
1493
|
+
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)}
|
|
1383
1494
|
`);
|
|
1384
1495
|
const h = hostInfo3();
|
|
1385
1496
|
io.out(`Created ${path} and ${join6(dir, "models")}.`);
|
|
@@ -1498,8 +1609,8 @@ async function bench(io, args, json) {
|
|
|
1498
1609
|
let cleanup;
|
|
1499
1610
|
if (file) path = resolve2(io.cwd, file);
|
|
1500
1611
|
else {
|
|
1501
|
-
const dir =
|
|
1502
|
-
cleanup = () =>
|
|
1612
|
+
const dir = mkdtempSync2(join6(tmpdir2(), "clutor-bench-"));
|
|
1613
|
+
cleanup = () => rmSync2(dir, { recursive: true, force: true });
|
|
1503
1614
|
path = join6(dir, "bench.gguf");
|
|
1504
1615
|
io.err("No --file given: writing a test model of about 1 GiB to the temp folder (it will be deleted).");
|
|
1505
1616
|
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 });
|
|
@@ -1512,7 +1623,7 @@ async function bench(io, args, json) {
|
|
|
1512
1623
|
const m = await measureExpertReads2(path, layout, h.totalRamBytes, 96);
|
|
1513
1624
|
const dm = diskModelFrom(m, expertSizeBytes2(layout), layout.layers.find((l) => l.experts.length)?.experts[0]?.length ?? 1);
|
|
1514
1625
|
const bin = findEngine();
|
|
1515
|
-
if (flagBool(args, "engine") && !bin) throw new UsageError("the native engine is not
|
|
1626
|
+
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)");
|
|
1516
1627
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1517
1628
|
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;
|
|
1518
1629
|
if (json) {
|
|
@@ -1637,7 +1748,7 @@ async function code(io, args, task, session) {
|
|
|
1637
1748
|
if (!task.trim()) throw new UsageError('usage: clutor code "<what to do>" [--dir folder]');
|
|
1638
1749
|
const { config, root } = cfgFor(args, io);
|
|
1639
1750
|
const dir = resolve2(io.cwd, flagString(args, "dir") ?? ".");
|
|
1640
|
-
if (!
|
|
1751
|
+
if (!existsSync2(dir)) throw new UsageError(`${dir} does not exist`);
|
|
1641
1752
|
const backend = session ? session.backend ??= await startBackendIfConfigured(config, io) : await startBackendIfConfigured(config, io);
|
|
1642
1753
|
try {
|
|
1643
1754
|
const llm = llmFrom(config, backend);
|
|
@@ -1942,7 +2053,7 @@ async function pull(io, args, id) {
|
|
|
1942
2053
|
const d = DOWNLOADS.find((x) => x.id === id);
|
|
1943
2054
|
if (!d) throw new UsageError(`unknown download "${id}"; run clutor pull to see the list`);
|
|
1944
2055
|
const dest = join6(dir, d.file);
|
|
1945
|
-
if (
|
|
2056
|
+
if (existsSync2(dest)) {
|
|
1946
2057
|
io.out(`${dest} is already there.`);
|
|
1947
2058
|
return 0;
|
|
1948
2059
|
}
|
|
@@ -1954,7 +2065,7 @@ async function pull(io, args, id) {
|
|
|
1954
2065
|
}
|
|
1955
2066
|
}
|
|
1956
2067
|
let last = 0;
|
|
1957
|
-
const bytes = await downloadFile(downloadUrl(d), dest, { expectedBytes: d.bytes, onProgress: (done, total) => {
|
|
2068
|
+
const bytes = await downloadFile(downloadUrl(d), dest, { expectedBytes: d.bytes, expectedSha256: d.sha256, onProgress: (done, total) => {
|
|
1958
2069
|
const p = Math.floor(done / total * 10);
|
|
1959
2070
|
if (p > last) {
|
|
1960
2071
|
last = p;
|
|
@@ -2013,10 +2124,91 @@ function formatComparison(c) {
|
|
|
2013
2124
|
table(rows, ["", ...c.map((x) => x.name)])
|
|
2014
2125
|
].join("\n");
|
|
2015
2126
|
}
|
|
2127
|
+
function engineFlags(args) {
|
|
2128
|
+
const out = [];
|
|
2129
|
+
const num = (n, lo, hi) => {
|
|
2130
|
+
const v = flagInt(args, n, lo, hi);
|
|
2131
|
+
if (v !== void 0) out.push(`--${n}`, String(v));
|
|
2132
|
+
};
|
|
2133
|
+
num("cache-mb", 1, 1e7);
|
|
2134
|
+
num("io-threads", 1, 16);
|
|
2135
|
+
num("prefetch-n", 1, 64);
|
|
2136
|
+
num("stage", 1, 1e5);
|
|
2137
|
+
const pol = flagString(args, "policy");
|
|
2138
|
+
if (pol) {
|
|
2139
|
+
if (!["lru", "wtinylfu", "adaptive"].includes(pol)) throw new UsageError("--policy must be lru, wtinylfu or adaptive");
|
|
2140
|
+
out.push("--policy", pol);
|
|
2141
|
+
}
|
|
2142
|
+
const pf = flagString(args, "prefetch");
|
|
2143
|
+
if (pf) {
|
|
2144
|
+
if (!["on", "off", "prev", "ahead"].includes(pf)) throw new UsageError("--prefetch must be off, prev or ahead");
|
|
2145
|
+
out.push("--prefetch", pf);
|
|
2146
|
+
}
|
|
2147
|
+
const sm = flagString(args, "smart");
|
|
2148
|
+
if (sm && sm !== "off") {
|
|
2149
|
+
if (!/^\d+,\d+,[\d.]+$/.test(sm)) throw new UsageError("--smart wants J,M,TAU, for example 4,16,0.02");
|
|
2150
|
+
out.push("--smart", sm);
|
|
2151
|
+
}
|
|
2152
|
+
if (flagBool(args, "no-os-cache")) out.push("--no-os-cache");
|
|
2153
|
+
return out;
|
|
2154
|
+
}
|
|
2155
|
+
async function generateCmd(io, args, file, prompt) {
|
|
2156
|
+
if (!file || !prompt.trim()) throw new UsageError('usage: clutor generate <model.gguf> "<prompt>" [--n N] [--cache-mb N] [--smart J,M,TAU]');
|
|
2157
|
+
const bin = findEngine();
|
|
2158
|
+
if (!bin) throw new UsageError("the native engine is not installed: run `clutor engine install` (prebuilt) or `clutor engine build` (needs a C compiler)");
|
|
2159
|
+
const path = resolve2(io.cwd, file);
|
|
2160
|
+
if (!existsSync2(path)) throw new UsageError(`${path} does not exist`);
|
|
2161
|
+
const n = flagInt(args, "n", 1, 1e5) ?? 64;
|
|
2162
|
+
const trace = flagString(args, "trace");
|
|
2163
|
+
const argv = ["generate", path, prompt, "--n", String(n), ...engineFlags(args), ...trace ? ["--trace", resolve2(io.cwd, trace)] : []];
|
|
2164
|
+
const r = await engineRun(bin, argv, (s) => (io.write ?? ((t) => io.out(t.replace(/\n$/, ""))))(s));
|
|
2165
|
+
io.write?.("\n");
|
|
2166
|
+
if (r.stderr.trim()) io.err(r.stderr.trim());
|
|
2167
|
+
if (r.code !== 0) {
|
|
2168
|
+
io.err(`the engine exited with ${r.code}`);
|
|
2169
|
+
return 1;
|
|
2170
|
+
}
|
|
2171
|
+
return 0;
|
|
2172
|
+
}
|
|
2173
|
+
async function qualityCmd(io, args, file, text, json) {
|
|
2174
|
+
if (!file || !text) throw new UsageError("usage: clutor quality <model.gguf> <text-file> --cache-mb N [--smart J,M,TAU] [--max-tokens N]");
|
|
2175
|
+
if (flagInt(args, "cache-mb", 1, 1e7) === void 0) throw new UsageError("quality compares against a streamed run: give --cache-mb N");
|
|
2176
|
+
const bin = findEngine();
|
|
2177
|
+
if (!bin) throw new UsageError("the native engine is not installed: run `clutor engine install` (prebuilt) or `clutor engine build` (needs a C compiler)");
|
|
2178
|
+
const path = resolve2(io.cwd, file), textPath = resolve2(io.cwd, text);
|
|
2179
|
+
if (!existsSync2(path)) throw new UsageError(`${path} does not exist`);
|
|
2180
|
+
if (!existsSync2(textPath)) throw new UsageError(`${textPath} does not exist`);
|
|
2181
|
+
const mt = flagInt(args, "max-tokens", 2, 1e6);
|
|
2182
|
+
const r = await run(bin, ["quality", path, textPath, ...engineFlags(args), ...mt ? ["--max-tokens", String(mt)] : []], { timeoutMs: 36e5 });
|
|
2183
|
+
if (r.code !== 0) {
|
|
2184
|
+
io.err(r.stderr.trim() || `the engine exited with ${r.code}`);
|
|
2185
|
+
return 1;
|
|
2186
|
+
}
|
|
2187
|
+
if (json) {
|
|
2188
|
+
io.out(r.stdout.trim());
|
|
2189
|
+
return 0;
|
|
2190
|
+
}
|
|
2191
|
+
let q;
|
|
2192
|
+
try {
|
|
2193
|
+
q = JSON.parse(r.stdout);
|
|
2194
|
+
} catch {
|
|
2195
|
+
throw new EngineError("the engine printed something that is not a report");
|
|
2196
|
+
}
|
|
2197
|
+
const n = (k) => q[k];
|
|
2198
|
+
io.out([
|
|
2199
|
+
`MEASURED ${n("tokensScored")} tokens scored, teacher-forced`,
|
|
2200
|
+
`MEASURED perplexity ${n("perplexityExact").toFixed(3)} exact routing, ${n("perplexityRun").toFixed(3)} this run (x${n("perplexityRatio").toFixed(4)})`,
|
|
2201
|
+
`MEASURED KL divergence from exact ${n("meanKL").toFixed(4)}; the top token is unchanged at ${(n("top1Agreement") * 100).toFixed(1)}% of positions`,
|
|
2202
|
+
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",
|
|
2203
|
+
`MEASURED expert cache hit rate ${(n("hitRate") * 100).toFixed(1)}%, ${n("gbPerToken").toFixed(3)} GB read per token`,
|
|
2204
|
+
"The exact reference maps the whole file, so the OS cache is warm: time is not reported here."
|
|
2205
|
+
].join("\n"));
|
|
2206
|
+
return 0;
|
|
2207
|
+
}
|
|
2016
2208
|
async function streamCmd(io, args, file, json) {
|
|
2017
2209
|
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]");
|
|
2018
2210
|
const bin = findEngine();
|
|
2019
|
-
if (!bin) throw new UsageError("the native engine is not
|
|
2211
|
+
if (!bin) throw new UsageError("the native engine is not installed: run `clutor engine install` (prebuilt) or `clutor engine build` (needs a C compiler)");
|
|
2020
2212
|
const num = (n, lo, hi) => flagInt(args, n, lo, hi);
|
|
2021
2213
|
const pol = flagString(args, "policy");
|
|
2022
2214
|
if (pol && !["lru", "wtinylfu", "adaptive"].includes(pol)) throw new UsageError("--policy must be lru, wtinylfu or adaptive");
|
|
@@ -2036,11 +2228,12 @@ async function streamCmd(io, args, file, json) {
|
|
|
2036
2228
|
...dn ? { dense: dn } : {},
|
|
2037
2229
|
...computeFlag(args) ? { compute: computeFlag(args) } : {},
|
|
2038
2230
|
...flagBool(args, "no-os-cache") ? { noOsCache: true } : {},
|
|
2231
|
+
...flagString(args, "trace") ? { trace: flagString(args, "trace") } : {},
|
|
2039
2232
|
...num("window", 1, 1e5) !== void 0 ? { window: num("window", 1, 1e5) } : {},
|
|
2040
2233
|
...shiftFlag(args)
|
|
2041
2234
|
};
|
|
2042
2235
|
const path = resolve2(io.cwd, file);
|
|
2043
|
-
if (!
|
|
2236
|
+
if (!existsSync2(path)) throw new UsageError(`${path} does not exist`);
|
|
2044
2237
|
const r = await engineStream(bin, path, o);
|
|
2045
2238
|
if (json) {
|
|
2046
2239
|
io.out(JSON.stringify(r, null, 2));
|
|
@@ -2065,7 +2258,7 @@ function describeStream(r) {
|
|
|
2065
2258
|
lines.push(`MEASURED hit rate and peak RSS per window of tokens${note}:
|
|
2066
2259
|
${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"])}`);
|
|
2067
2260
|
}
|
|
2068
|
-
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.`);
|
|
2261
|
+
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.`);
|
|
2069
2262
|
return lines.join("\n");
|
|
2070
2263
|
}
|
|
2071
2264
|
async function engineCmd(io, sub) {
|
|
@@ -2087,7 +2280,20 @@ async function engineCmd(io, sub) {
|
|
|
2087
2280
|
throw e;
|
|
2088
2281
|
}
|
|
2089
2282
|
}
|
|
2090
|
-
|
|
2283
|
+
if (sub === "install") {
|
|
2284
|
+
io.err("Downloading the prebuilt native engine and checking its SHA-256...");
|
|
2285
|
+
try {
|
|
2286
|
+
io.out(`Installed ${await installPrebuilt(version())}`);
|
|
2287
|
+
return 0;
|
|
2288
|
+
} catch (e) {
|
|
2289
|
+
if (e instanceof EngineError) {
|
|
2290
|
+
io.err(e.message);
|
|
2291
|
+
return 1;
|
|
2292
|
+
}
|
|
2293
|
+
throw e;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
throw new UsageError("usage: clutor engine install | build | where");
|
|
2091
2297
|
}
|
|
2092
2298
|
async function engineSlashCommand(io, config, what) {
|
|
2093
2299
|
const file = config.backend.llamaServer.model;
|
|
@@ -2156,8 +2362,11 @@ export {
|
|
|
2156
2362
|
buildEngine,
|
|
2157
2363
|
engineInfo,
|
|
2158
2364
|
engineStream,
|
|
2365
|
+
engineRun,
|
|
2366
|
+
enginePlatform,
|
|
2367
|
+
installPrebuilt,
|
|
2159
2368
|
version,
|
|
2160
2369
|
main,
|
|
2161
2370
|
startBackendIfConfigured
|
|
2162
2371
|
};
|
|
2163
|
-
//# sourceMappingURL=chunk-
|
|
2372
|
+
//# sourceMappingURL=chunk-2FBPYNHP.js.map
|