offgrid-ai 0.3.19 → 0.3.21
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/package.json +1 -1
- package/src/backends.mjs +19 -10
- package/src/cli.mjs +120 -84
- package/src/profile-setup.mjs +30 -57
- package/src/ui.mjs +45 -2
package/package.json
CHANGED
package/src/backends.mjs
CHANGED
|
@@ -72,16 +72,18 @@ async function scanOllamaModels() {
|
|
|
72
72
|
if (!response.ok) return [];
|
|
73
73
|
const body = await response.json();
|
|
74
74
|
if (!Array.isArray(body?.models)) return [];
|
|
75
|
-
return body.models
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
75
|
+
return body.models
|
|
76
|
+
.filter((model) => isLocalOllamaModel(model))
|
|
77
|
+
.map((model) => ({
|
|
78
|
+
id: model.name,
|
|
79
|
+
label: ollamaLabel(model.name),
|
|
80
|
+
aliasSuggestion: model.name,
|
|
81
|
+
sizeBytes: model.size ?? 0,
|
|
82
|
+
quant: model.details?.quantization_level,
|
|
83
|
+
family: model.details?.family,
|
|
84
|
+
backend: "ollama",
|
|
85
|
+
source: "ollama",
|
|
86
|
+
})).sort((a, b) => a.label.localeCompare(b.label));
|
|
85
87
|
} catch {
|
|
86
88
|
return [];
|
|
87
89
|
}
|
|
@@ -112,6 +114,13 @@ async function scanOmlxModels() {
|
|
|
112
114
|
|
|
113
115
|
// ── Labels ──────────────────────────────────────────────────────────────
|
|
114
116
|
|
|
117
|
+
function isLocalOllamaModel(model) {
|
|
118
|
+
const name = String(model?.name ?? "");
|
|
119
|
+
if (/:cloud(?:$|\b)/i.test(name)) return false;
|
|
120
|
+
if (!Number.isFinite(model?.size) || model.size <= 0) return false;
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
115
124
|
function ollamaLabel(name) {
|
|
116
125
|
return name.replace(/[-_]/g, " ").replace(/^gemma\b/i, "Gemma").replace(/^qwen/i, "Qwen");
|
|
117
126
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { startServer, stopProfile, waitForReady, serverReady, isProfileRunning,
|
|
|
9
9
|
import { syncPiConfig, removeFromPiConfig, hasPiModel, launchPi, hasPi } from "./harness-pi.mjs";
|
|
10
10
|
import { tailFriendly } from "./logs.mjs";
|
|
11
11
|
import { estimateMemory } from "./estimate.mjs";
|
|
12
|
-
import { pc, formatBytes, renderRows, renderSection, startInteractive, createPrompt, parseOptions } from "./ui.mjs";
|
|
12
|
+
import { pc, formatBytes, renderRows, renderSection, renderCard, humanCapabilitySummary, statusText, startInteractive, createPrompt, parseOptions } from "./ui.mjs";
|
|
13
13
|
import { checkForUpdate, currentPackageVersion, detectInvocation, updateCommand, runUpdateCommand } from "./updates.mjs";
|
|
14
14
|
import { removeInstallerPathEntries } from "./shell-path.mjs";
|
|
15
15
|
import { configureLocalProfile } from "./profile-setup.mjs";
|
|
@@ -185,12 +185,12 @@ async function modelCommandCenter(catalog) {
|
|
|
185
185
|
|
|
186
186
|
const prompt = createPrompt();
|
|
187
187
|
try {
|
|
188
|
-
const action = await prompt.choice("What
|
|
189
|
-
{ value: "
|
|
190
|
-
{ value: "setup", label: "Set up
|
|
191
|
-
{ value: "
|
|
192
|
-
{ value: "benchmark", label: "Benchmark", hint: "Coming soon
|
|
193
|
-
{ value: "remove", label: "Remove", hint: "Delete a
|
|
188
|
+
const action = await prompt.choice("What would you like to do?", [
|
|
189
|
+
{ value: "run", label: "Start chatting", hint: "Start a local model and open Pi" },
|
|
190
|
+
{ value: "setup", label: "Set up a downloaded model", hint: "One-time setup or Pi sync" },
|
|
191
|
+
{ value: "inspect", label: "See model details", hint: "Show advanced paths, ports, and flags" },
|
|
192
|
+
{ value: "benchmark", label: "Benchmark", hint: "Coming soon" },
|
|
193
|
+
{ value: "remove", label: "Remove a saved setup", hint: "Delete a model setup from offgrid-ai" },
|
|
194
194
|
], "run");
|
|
195
195
|
if (action === "benchmark") return await benchmarkFlow();
|
|
196
196
|
const item = await chooseCatalogItem(prompt, items, action);
|
|
@@ -240,33 +240,38 @@ async function printModelCatalog({ profiles, newModels, managedItems }, items =
|
|
|
240
240
|
const index = items.findIndex(predicate);
|
|
241
241
|
return index === -1 ? " " : String(index + 1).padStart(2, " ");
|
|
242
242
|
};
|
|
243
|
+
const runningProfilesNow = [];
|
|
244
|
+
for (const profile of profiles) {
|
|
245
|
+
if (await isProfileRunning(profile)) runningProfilesNow.push(profile);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log("\n" + renderCard("Your local AI workspace", renderRows([
|
|
249
|
+
["Ready to chat", pc.green(`${profiles.length} saved setup${profiles.length === 1 ? "" : "s"}`)],
|
|
250
|
+
["Need setup", newModels.length > 0 ? pc.yellow(`${newModels.length} downloaded model${newModels.length === 1 ? "" : "s"}`) : pc.green("none")],
|
|
251
|
+
["Running now", runningProfilesNow.length > 0 ? pc.green(String(runningProfilesNow.length)) : pc.dim("none")],
|
|
252
|
+
["Next step", profiles.length > 0 ? "Start chatting" : newModels.length > 0 ? "Set up a downloaded model" : "Download a model"],
|
|
253
|
+
]), { formatBorder: pc.cyan }));
|
|
243
254
|
|
|
244
|
-
console.log(pc.bold("
|
|
255
|
+
console.log("\n" + pc.bold("Ready to chat"));
|
|
245
256
|
if (profiles.length === 0) {
|
|
246
|
-
console.log(
|
|
257
|
+
console.log(renderCard("No saved setups yet", "Downloaded models will appear below. Set one up once, then it will be ready from here.", { formatBorder: pc.yellow }));
|
|
247
258
|
} else {
|
|
248
259
|
for (const profile of profiles) {
|
|
249
|
-
const
|
|
250
|
-
const colorMap = { "llama-cpp": pc.yellow, "llama-cpp-mtp": pc.blue, "ollama": pc.magenta, "omlx": pc.cyan };
|
|
251
|
-
const running = await isProfileRunning(profile);
|
|
260
|
+
const running = runningProfilesNow.some((item) => item.id === profile.id);
|
|
252
261
|
const piConfigured = await hasPiModel(profile);
|
|
253
|
-
const c = colorMap[profile.backend] ?? pc.magenta;
|
|
254
262
|
const num = itemNumber((item) => item.type === "profile" && item.profile.id === profile.id);
|
|
255
|
-
console.log(
|
|
263
|
+
console.log(profileCatalogCard(num, profile, { running, piConfigured }));
|
|
256
264
|
}
|
|
257
265
|
}
|
|
258
266
|
|
|
259
|
-
console.log("");
|
|
260
|
-
console.log(pc.bold("Downloaded models not set up yet"));
|
|
267
|
+
console.log("\n" + pc.bold("Downloaded, needs one-time setup"));
|
|
261
268
|
if (newModels.length === 0) {
|
|
262
|
-
console.log(
|
|
269
|
+
console.log(renderCard("All set", "Every downloaded local model already has a saved setup.", { formatBorder: pc.green }));
|
|
263
270
|
} else {
|
|
264
271
|
for (const model of newModels.slice(0, 20)) {
|
|
265
272
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
266
273
|
const num = itemNumber((item) => item.type === "new" && item.model.path === model.path);
|
|
267
|
-
console.log(
|
|
268
|
-
console.log(` alias: ${pc.cyan(model.aliasSuggestion)}`);
|
|
269
|
-
console.log(` size: ${formatBytes(model.sizeBytes)}`);
|
|
274
|
+
console.log(downloadedModelCard(num, model, caps));
|
|
270
275
|
}
|
|
271
276
|
if (newModels.length > 20) console.log(pc.dim(` ... and ${newModels.length - 20} more`));
|
|
272
277
|
}
|
|
@@ -275,17 +280,45 @@ async function printModelCatalog({ profiles, newModels, managedItems }, items =
|
|
|
275
280
|
const backendItems = managedItems.filter((item) => item.backendId === backendId);
|
|
276
281
|
if (backendItems.length === 0) continue;
|
|
277
282
|
const be = BACKENDS[backendId];
|
|
278
|
-
console.log("");
|
|
279
|
-
console.log(pc.bold(`${be.label} models`));
|
|
283
|
+
console.log("\n" + pc.bold(`Local models via ${be.label}`));
|
|
280
284
|
for (const { model } of backendItems.slice(0, 10)) {
|
|
281
285
|
const num = itemNumber((item) => item.type === "managed" && item.backendId === backendId && item.model.id === model.id);
|
|
282
|
-
console.log(
|
|
283
|
-
console.log(` id: ${pc.cyan(model.id)}`);
|
|
286
|
+
console.log(managedModelCard(num, model, be));
|
|
284
287
|
}
|
|
285
288
|
if (backendItems.length > 10) console.log(pc.dim(` ... and ${backendItems.length - 10} more`));
|
|
286
289
|
}
|
|
287
290
|
}
|
|
288
291
|
|
|
292
|
+
function profileCatalogCard(num, profile, { running, piConfigured }) {
|
|
293
|
+
const backend = backendFor(profile.backend);
|
|
294
|
+
const caps = profile.capabilities ?? {};
|
|
295
|
+
const status = running ? statusText("running", "Running now") : statusText("ready", "Ready to chat");
|
|
296
|
+
return renderCard(`${num}. ${profile.label}`, renderRows([
|
|
297
|
+
["Status", status],
|
|
298
|
+
["Good for", humanCapabilitySummary(caps)],
|
|
299
|
+
["Pi", piConfigured ? pc.green("Synced") : pc.yellow("Needs sync")],
|
|
300
|
+
["Runs with", backend.label],
|
|
301
|
+
]), { formatBorder: running ? pc.green : pc.cyan });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function downloadedModelCard(num, model, caps) {
|
|
305
|
+
return renderCard(`${num}. ${model.label}`, renderRows([
|
|
306
|
+
["Status", statusText("warning", "Needs one-time setup")],
|
|
307
|
+
["Good for", humanCapabilitySummary(caps)],
|
|
308
|
+
["Size", formatBytes(model.sizeBytes)],
|
|
309
|
+
["When selected", "offgrid-ai will recommend safe local settings"],
|
|
310
|
+
]), { formatBorder: pc.yellow });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function managedModelCard(num, model, backend) {
|
|
314
|
+
return renderCard(`${num}. ${model.label}`, renderRows([
|
|
315
|
+
["Status", statusText("info", `Local model via ${backend.label}`)],
|
|
316
|
+
["Runs with", backend.label],
|
|
317
|
+
["Model ID", pc.cyan(model.id)],
|
|
318
|
+
...(model.quant ? [["Size/type", model.quant]] : []),
|
|
319
|
+
]), { formatBorder: pc.magenta });
|
|
320
|
+
}
|
|
321
|
+
|
|
289
322
|
function modelCatalogItems({ profiles, newModels, managedItems }) {
|
|
290
323
|
return [
|
|
291
324
|
...profiles.map((profile) => ({ type: "profile", profile, label: profile.label, hint: `${profile.modelAlias} · ${profile.baseUrl}` })),
|
|
@@ -300,7 +333,7 @@ async function chooseCatalogItem(prompt, items, action) {
|
|
|
300
333
|
return null;
|
|
301
334
|
}
|
|
302
335
|
|
|
303
|
-
const input = await prompt.text("
|
|
336
|
+
const input = await prompt.text(action === "remove" ? "Which saved setup should be removed? Enter its number" : "Which model? Enter its number", "");
|
|
304
337
|
if (!input) return null;
|
|
305
338
|
const index = Number(input) - 1;
|
|
306
339
|
if (!Number.isInteger(index) || index < 0 || index >= items.length) {
|
|
@@ -360,43 +393,53 @@ async function printProfileDetails(profile) {
|
|
|
360
393
|
const backend = backendFor(profile.backend);
|
|
361
394
|
const isManaged = backend.type === "managed-server";
|
|
362
395
|
const piConfigured = await hasPiModel(profile);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
["
|
|
366
|
-
["
|
|
396
|
+
const running = await isProfileRunning(profile);
|
|
397
|
+
console.log("\n" + renderSection("Model overview", renderRows([
|
|
398
|
+
["Name", pc.bold(profile.label)],
|
|
399
|
+
["Status", running ? pc.green("Running now") : pc.green("Ready to chat")],
|
|
400
|
+
["Good for", humanCapabilitySummary(profile.capabilities ?? {})],
|
|
401
|
+
["Pi", piConfigured ? pc.green("Synced") : pc.yellow("Needs sync")],
|
|
402
|
+
["Server", pc.green(profile.baseUrl)],
|
|
403
|
+
])));
|
|
404
|
+
|
|
405
|
+
console.log("\n" + renderSection("Advanced details", renderRows([
|
|
406
|
+
["Setup ID", pc.cyan(profile.id)],
|
|
407
|
+
["Runs with", backend.label],
|
|
408
|
+
["Model alias", pc.cyan(profile.modelAlias)],
|
|
367
409
|
...(profile.capabilities ? [["Detected", capabilitySummary(profile.capabilities)]] : []),
|
|
368
|
-
["Endpoint", pc.green(profile.baseUrl)],
|
|
369
410
|
...(!isManaged ? [
|
|
370
|
-
["
|
|
371
|
-
["
|
|
372
|
-
["
|
|
411
|
+
["Local file", profile.modelPath ?? "unknown"],
|
|
412
|
+
["Vision file", profile.mmprojPath ?? "none"],
|
|
413
|
+
["Model size", profile.modelPath && existsSync(profile.modelPath) ? formatBytes(statSync(profile.modelPath).size) : "unknown"],
|
|
373
414
|
] : []),
|
|
374
|
-
["Alias", pc.cyan(profile.modelAlias)],
|
|
375
|
-
["Pi", piConfigured ? pc.green("configured") : pc.yellow("not synced")],
|
|
376
415
|
])));
|
|
377
416
|
|
|
378
417
|
if (!isManaged && profile.commandArgv) {
|
|
379
|
-
console.log("\n" +
|
|
380
|
-
console.log(pc.dim(buildPrettyCommand(profile)));
|
|
418
|
+
console.log("\n" + renderSection("Advanced command", pc.dim(buildPrettyCommand(profile))));
|
|
381
419
|
}
|
|
382
420
|
}
|
|
383
421
|
|
|
384
422
|
function printGgufModelDetails(model) {
|
|
385
423
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
386
|
-
console.log("\n" + renderSection("
|
|
387
|
-
["
|
|
424
|
+
console.log("\n" + renderSection("Downloaded model", renderRows([
|
|
425
|
+
["Name", pc.bold(model.label)],
|
|
426
|
+
["Status", pc.yellow("Needs one-time setup")],
|
|
427
|
+
["Good for", humanCapabilitySummary(caps)],
|
|
428
|
+
["Size", formatBytes(model.sizeBytes)],
|
|
429
|
+
])));
|
|
430
|
+
console.log("\n" + renderSection("Advanced details", renderRows([
|
|
431
|
+
["Local file", model.path],
|
|
432
|
+
["Vision file", model.mmprojPath ?? "none"],
|
|
388
433
|
["Detected", capabilitySummary(caps)],
|
|
389
|
-
["Model", model.path],
|
|
390
|
-
["MMProj", model.mmprojPath ?? "none"],
|
|
391
434
|
["Quant", model.quant ?? "unknown"],
|
|
392
|
-
["Size", formatBytes(model.sizeBytes)],
|
|
393
435
|
])));
|
|
394
436
|
}
|
|
395
437
|
|
|
396
438
|
function printManagedModelDetails(model, backend) {
|
|
397
439
|
console.log("\n" + renderSection(`${backend.label} model`, renderRows([
|
|
398
|
-
["
|
|
399
|
-
["
|
|
440
|
+
["Name", pc.bold(model.label)],
|
|
441
|
+
["Status", pc.green("Available from another app")],
|
|
442
|
+
["Model ID", pc.cyan(model.id)],
|
|
400
443
|
["Quant", model.quant ?? "unknown"],
|
|
401
444
|
["Family", model.family ?? "unknown"],
|
|
402
445
|
])));
|
|
@@ -414,16 +457,6 @@ function capabilitySummary(caps) {
|
|
|
414
457
|
return parts.length > 0 ? parts.join(" · ") : "standard GGUF";
|
|
415
458
|
}
|
|
416
459
|
|
|
417
|
-
function capabilityBadges(caps) {
|
|
418
|
-
const badges = [];
|
|
419
|
-
if (caps.mtp) badges.push(pc.blue("[MTP]"));
|
|
420
|
-
if (caps.qat) badges.push(pc.green("[QAT]"));
|
|
421
|
-
|
|
422
|
-
if (caps.thinking) badges.push(pc.magenta("[thinking]"));
|
|
423
|
-
if (caps.vision) badges.push(pc.cyan("[vision]"));
|
|
424
|
-
return badges.join(" ");
|
|
425
|
-
}
|
|
426
|
-
|
|
427
460
|
function createManagedProfile(model, backendId) {
|
|
428
461
|
return normalizeProfile({
|
|
429
462
|
id: model.id.replace(/[^a-z0-9._-]+/gi, "-").toLowerCase(),
|
|
@@ -487,10 +520,10 @@ async function runProfile(profile, options = {}) {
|
|
|
487
520
|
if (!isManaged && profile.modelPath && existsSync(profile.modelPath)) {
|
|
488
521
|
try {
|
|
489
522
|
const est = estimateMemory(profile.modelPath, profile.mmprojPath, null, profile.flags);
|
|
490
|
-
console.log(renderSection("Memory", renderRows([
|
|
523
|
+
console.log(renderSection("Memory estimate", renderRows([
|
|
491
524
|
["Estimated total", pc.bold(`~${formatBytes(est.totalBytes)}`)],
|
|
492
|
-
["Model", formatBytes(est.modelBytes)],
|
|
493
|
-
["
|
|
525
|
+
["Model file", formatBytes(est.modelBytes)],
|
|
526
|
+
["Conversation memory", est.kvBytes ? `~${formatBytes(est.kvBytes)}` : "unknown"],
|
|
494
527
|
])));
|
|
495
528
|
} catch { /* estimate failed, skip */ }
|
|
496
529
|
}
|
|
@@ -541,9 +574,11 @@ async function removeProfileInteractive(id) {
|
|
|
541
574
|
// ── Benchmark (stub) ────────────────────────────────────────────────────────
|
|
542
575
|
|
|
543
576
|
async function benchmarkFlow() {
|
|
544
|
-
console.log(
|
|
545
|
-
|
|
546
|
-
|
|
577
|
+
console.log("\n" + renderCard("Benchmark", renderRows([
|
|
578
|
+
["Status", pc.yellow("Coming soon")],
|
|
579
|
+
["What it will do", "Compare local models with repeatable prompts"],
|
|
580
|
+
["For now", "Start a model with offgrid-ai, then run benchmarks manually"],
|
|
581
|
+
]), { formatBorder: pc.yellow }));
|
|
547
582
|
}
|
|
548
583
|
|
|
549
584
|
// ── Status ──────────────────────────────────────────────────────────────────
|
|
@@ -562,21 +597,27 @@ async function statusCommand() {
|
|
|
562
597
|
const running = statuses.filter((s) => s.status.running);
|
|
563
598
|
|
|
564
599
|
if (running.length === 0) {
|
|
565
|
-
console.log(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
600
|
+
console.log(renderCard("Status", renderRows([
|
|
601
|
+
["Running now", pc.dim("none")],
|
|
602
|
+
["Ready setups", profiles.length > 0 ? pc.green(String(profiles.length)) : pc.dim("none")],
|
|
603
|
+
["Next step", profiles.length > 0 ? "Run offgrid-ai to start chatting" : "Run offgrid-ai to set up a model"],
|
|
604
|
+
]), { formatBorder: profiles.length > 0 ? pc.cyan : pc.yellow }));
|
|
569
605
|
return;
|
|
570
606
|
}
|
|
571
607
|
|
|
572
|
-
console.log(
|
|
608
|
+
console.log(renderCard("Status", renderRows([
|
|
609
|
+
["Running now", pc.green(`${running.length} model${running.length === 1 ? "" : "s"}`)],
|
|
610
|
+
["Stop", "offgrid-ai stop"],
|
|
611
|
+
]), { formatBorder: pc.green }));
|
|
573
612
|
for (const { profile, status } of running) {
|
|
574
613
|
const backend = backendFor(profile.backend);
|
|
575
|
-
console.log(
|
|
576
|
-
|
|
577
|
-
|
|
614
|
+
console.log("\n" + renderCard(profile.label, renderRows([
|
|
615
|
+
["Status", status.ready ? pc.green("Ready") : pc.yellow("Starting up")],
|
|
616
|
+
["Runs with", backend.label],
|
|
617
|
+
["Process", `pid ${status.pid}`],
|
|
618
|
+
["Server", profile.baseUrl],
|
|
619
|
+
]), { formatBorder: status.ready ? pc.green : pc.yellow }));
|
|
578
620
|
}
|
|
579
|
-
console.log(pc.dim("\nStop with: offgrid-ai stop"));
|
|
580
621
|
}
|
|
581
622
|
|
|
582
623
|
// ── Stop ────────────────────────────────────────────────────────────────────
|
|
@@ -1062,19 +1103,14 @@ async function printVersion() {
|
|
|
1062
1103
|
}
|
|
1063
1104
|
|
|
1064
1105
|
function printHelp() {
|
|
1065
|
-
console.log(
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
offgrid-ai
|
|
1074
|
-
|
|
1075
|
-
Flags:
|
|
1076
|
-
--verbose Show install output (brew, lms, ollama, etc.)
|
|
1077
|
-
|
|
1078
|
-
First run? offgrid-ai walks you through installing everything you need.
|
|
1079
|
-
After that, just run it — it finds your models, auto-configures, and launches Pi.`);
|
|
1106
|
+
console.log(renderCard("offgrid-ai", renderRows([
|
|
1107
|
+
["What it is", "A privacy-first local AI runner"],
|
|
1108
|
+
["Start", pc.bold("offgrid-ai")],
|
|
1109
|
+
["Status", "offgrid-ai status"],
|
|
1110
|
+
["Stop", "offgrid-ai stop"],
|
|
1111
|
+
["Uninstall", "offgrid-ai uninstall"],
|
|
1112
|
+
["Version", "offgrid-ai version"],
|
|
1113
|
+
]), { formatBorder: pc.cyan }));
|
|
1114
|
+
console.log("\n" + renderCard("How it works", "Run offgrid-ai, choose a local model, and start chatting in Pi.\n\nFirst run walks you through missing tools. After that, offgrid-ai remembers your model setup.", { formatBorder: pc.magenta }));
|
|
1115
|
+
console.log("\n" + pc.dim("Tip: use --verbose only when you want detailed install output."));
|
|
1080
1116
|
}
|
package/src/profile-setup.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { estimateMemory } from "./estimate.mjs";
|
|
2
|
-
import { pc, formatBytes, renderRows, renderSection } from "./ui.mjs";
|
|
2
|
+
import { pc, formatBytes, renderRows, renderSection, humanCapabilitySummary } from "./ui.mjs";
|
|
3
3
|
|
|
4
4
|
const CACHE_CHOICES = [
|
|
5
|
-
{ value: "bf16", label: "
|
|
6
|
-
{ value: "f16", label: "
|
|
7
|
-
{ value: "q8_0", label: "
|
|
8
|
-
{ value: "q4_0", label: "
|
|
5
|
+
{ value: "bf16", label: "Balanced", hint: "recommended: stable, good quality" },
|
|
6
|
+
{ value: "f16", label: "Compatible", hint: "stable fallback, similar memory use" },
|
|
7
|
+
{ value: "q8_0", label: "Lower memory", hint: "usually safe, uses less memory" },
|
|
8
|
+
{ value: "q4_0", label: "Smallest memory", hint: "maximum savings, quality/speed tradeoff" },
|
|
9
9
|
];
|
|
10
10
|
|
|
11
11
|
const GENERAL_DEFAULTS = {
|
|
@@ -26,64 +26,49 @@ export async function configureLocalProfile(prompt, profile) {
|
|
|
26
26
|
const caps = profile.capabilities ?? {};
|
|
27
27
|
|
|
28
28
|
console.log("");
|
|
29
|
-
console.log(renderSection("
|
|
29
|
+
console.log(renderSection("Let's set up this model", renderRows([
|
|
30
30
|
["Model", pc.bold(profile.label)],
|
|
31
|
-
["
|
|
32
|
-
["
|
|
33
|
-
["
|
|
34
|
-
["
|
|
31
|
+
["Good for", humanCapabilitySummary(caps)],
|
|
32
|
+
["Conversation memory", `${profile.flags.ctxSize.toLocaleString()} tokens`],
|
|
33
|
+
["Memory mode", `${profile.flags.cacheTypeK}/${profile.flags.cacheTypeV}`],
|
|
34
|
+
["Response style", samplingSummary(profile.flags)],
|
|
35
35
|
])));
|
|
36
|
-
console.log(pc.dim("
|
|
37
|
-
console.log(pc.dim("Sampling defaults are shown for transparency; you can edit command.json later if needed.\n"));
|
|
36
|
+
console.log(pc.dim("You can accept the recommended settings. Bigger conversation memory uses more RAM.\n"));
|
|
38
37
|
|
|
39
38
|
if (caps.mtp) {
|
|
40
|
-
console.log(renderSection("
|
|
41
|
-
|
|
42
|
-
["Port", "8081"],
|
|
43
|
-
["Flags", "--spec-type draft-mtp --spec-draft-n-max 2"],
|
|
44
|
-
])));
|
|
45
|
-
const useMtp = await prompt.yesNo("Use MTP speculative decoding flags?", true);
|
|
39
|
+
console.log(renderSection("MTP available", "This model supports multi-token prediction (MTP). offgrid-ai can run it with llama.cpp MTP on port 8081."));
|
|
40
|
+
const useMtp = await prompt.yesNo("Use MTP for this model?", true);
|
|
46
41
|
configured = useMtp ? applyMtpDefaults(configured) : removeMtpDefaults(configured);
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
if (caps.qat) {
|
|
50
45
|
console.log("");
|
|
51
|
-
console.log(renderSection("
|
|
52
|
-
["Meaning", "quantization-aware trained"],
|
|
53
|
-
["Runtime flags", "none required"],
|
|
54
|
-
])));
|
|
46
|
+
console.log(renderSection("QAT model", "This model is marked as quantization-aware trained (QAT). No extra runtime settings are needed."));
|
|
55
47
|
}
|
|
56
48
|
|
|
57
49
|
if (caps.thinking) {
|
|
58
50
|
console.log("");
|
|
59
|
-
console.log(renderSection("
|
|
60
|
-
|
|
61
|
-
["Flags", "--top-k 64 --presence-penalty 0 --repeat-penalty 1.1"],
|
|
62
|
-
["Template", "--chat-template-kwargs { enable_thinking: true }"],
|
|
63
|
-
])));
|
|
64
|
-
const useThinking = await prompt.yesNo("Use these thinking/loop-safe defaults?", true);
|
|
51
|
+
console.log(renderSection("Reasoning mode", "This model can reason step by step. offgrid-ai can use safer defaults that reduce repetitive loops."));
|
|
52
|
+
const useThinking = await prompt.yesNo("Use reasoning-friendly defaults?", true);
|
|
65
53
|
configured = useThinking ? applyThinkingDefaults(configured) : removeThinkingDefaults(configured);
|
|
66
54
|
}
|
|
67
55
|
|
|
68
|
-
const ctxSize = await prompt.number("
|
|
69
|
-
const cacheTypeK = await prompt.choice("
|
|
70
|
-
const cacheTypeV = await prompt.choice("
|
|
56
|
+
const ctxSize = await prompt.number("Conversation memory tokens", configured.flags.ctxSize, 1024, 1048576);
|
|
57
|
+
const cacheTypeK = await prompt.choice("Memory mode, part 1", CACHE_CHOICES, configured.flags.cacheTypeK);
|
|
58
|
+
const cacheTypeV = await prompt.choice("Memory mode, part 2", CACHE_CHOICES, configured.flags.cacheTypeV);
|
|
71
59
|
configured = applyRuntimeFlagOverrides(configured, { ctxSize, cacheTypeK, cacheTypeV });
|
|
72
60
|
|
|
73
61
|
console.log("");
|
|
74
|
-
console.log(renderSection("
|
|
75
|
-
["
|
|
76
|
-
["
|
|
77
|
-
["
|
|
78
|
-
["
|
|
79
|
-
["
|
|
80
|
-
["Min-p", configured.flags.minP],
|
|
81
|
-
["Presence penalty", configured.flags.presencePenalty],
|
|
82
|
-
["Repeat penalty", configured.flags.repeatPenalty],
|
|
62
|
+
console.log(renderSection("Final setup", renderRows([
|
|
63
|
+
["Runs with", configured.backend],
|
|
64
|
+
["Local address", configured.baseUrl],
|
|
65
|
+
["Creativity", configured.flags.temperature],
|
|
66
|
+
["Focus", configured.flags.topP],
|
|
67
|
+
["Reasoning breadth", configured.flags.topK],
|
|
83
68
|
])));
|
|
84
69
|
|
|
85
70
|
console.log("\n" + renderMemoryEstimate(configured));
|
|
86
|
-
if (!(await prompt.yesNo("Save
|
|
71
|
+
if (!(await prompt.yesNo("Save this model setup?", true))) return null;
|
|
87
72
|
return configured;
|
|
88
73
|
}
|
|
89
74
|
|
|
@@ -168,29 +153,17 @@ function removeOption(argv, flag) {
|
|
|
168
153
|
function renderMemoryEstimate(profile) {
|
|
169
154
|
try {
|
|
170
155
|
const est = estimateMemory(profile.modelPath, profile.mmprojPath, null, profile.flags);
|
|
171
|
-
return renderSection("Memory", renderRows([
|
|
156
|
+
return renderSection("Memory estimate", renderRows([
|
|
172
157
|
["Estimated total", pc.bold(`~${formatBytes(est.totalBytes)}`)],
|
|
173
|
-
["Model", formatBytes(est.modelBytes)],
|
|
174
|
-
["
|
|
158
|
+
["Model file", formatBytes(est.modelBytes)],
|
|
159
|
+
["Conversation memory", est.kvBytes ? `~${formatBytes(est.kvBytes)} (${profile.flags.ctxSize.toLocaleString()} tokens, ${profile.flags.cacheTypeK}/${profile.flags.cacheTypeV})` : "unknown"],
|
|
175
160
|
...(est.note ? [["Note", pc.yellow(est.note)]] : []),
|
|
176
161
|
]));
|
|
177
162
|
} catch {
|
|
178
|
-
return renderSection("Memory", pc.dim("Estimate unavailable for this model."));
|
|
163
|
+
return renderSection("Memory estimate", pc.dim("Estimate unavailable for this model."));
|
|
179
164
|
}
|
|
180
165
|
}
|
|
181
166
|
|
|
182
|
-
function detectionSummary(caps) {
|
|
183
|
-
const parts = [];
|
|
184
|
-
if (caps.architecture) parts.push(caps.architecture);
|
|
185
|
-
if (caps.quant) parts.push(caps.quant);
|
|
186
|
-
if (caps.mtp) parts.push("MTP");
|
|
187
|
-
if (caps.qat) parts.push("QAT");
|
|
188
|
-
|
|
189
|
-
if (caps.thinking) parts.push("thinking");
|
|
190
|
-
if (caps.vision) parts.push("vision");
|
|
191
|
-
return parts.length > 0 ? parts.join(" · ") : "standard GGUF";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
167
|
function samplingSummary(flags) {
|
|
195
168
|
return `temp ${flags.temperature}, top-p ${flags.topP}, top-k ${flags.topK}`;
|
|
196
169
|
}
|
package/src/ui.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cancel, confirm, intro, isCancel, select, text } from "@clack/prompts";
|
|
1
|
+
import { box, cancel, confirm, intro, isCancel, select, text } from "@clack/prompts";
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { stripVTControlCharacters } from "node:util";
|
|
4
4
|
|
|
@@ -59,8 +59,51 @@ export function renderRows(rows) {
|
|
|
59
59
|
}).join("\n");
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export function renderCard(title, body, options = {}) {
|
|
63
|
+
let output = "";
|
|
64
|
+
box(String(body ?? ""), title, {
|
|
65
|
+
output: captureOutput((chunk) => { output += chunk; }, options.columns),
|
|
66
|
+
withGuide: false,
|
|
67
|
+
width: "auto",
|
|
68
|
+
contentPadding: options.contentPadding ?? 1,
|
|
69
|
+
titlePadding: options.titlePadding ?? 1,
|
|
70
|
+
rounded: options.rounded ?? true,
|
|
71
|
+
titleAlign: options.titleAlign ?? "left",
|
|
72
|
+
contentAlign: options.contentAlign ?? "left",
|
|
73
|
+
formatBorder: options.formatBorder ?? pc.magenta,
|
|
74
|
+
});
|
|
75
|
+
return output.trimEnd();
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
export function renderSection(title, body) {
|
|
63
|
-
return
|
|
79
|
+
return renderCard(title, body, { formatBorder: pc.magenta });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function humanCapabilitySummary(caps = {}) {
|
|
83
|
+
const parts = [];
|
|
84
|
+
if (caps.thinking) parts.push(pc.magenta("Reasoning"));
|
|
85
|
+
if (caps.vision) parts.push(pc.cyan("Vision"));
|
|
86
|
+
if (caps.mtp) parts.push(pc.blue("MTP"));
|
|
87
|
+
if (caps.qat) parts.push(pc.green("QAT"));
|
|
88
|
+
return parts.length > 0 ? parts.join(" · ") : "General chat";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function statusText(kind, text) {
|
|
92
|
+
const color = {
|
|
93
|
+
ready: pc.green,
|
|
94
|
+
running: pc.green,
|
|
95
|
+
warning: pc.yellow,
|
|
96
|
+
info: pc.cyan,
|
|
97
|
+
muted: pc.dim,
|
|
98
|
+
}[kind] ?? ((value) => value);
|
|
99
|
+
return color(text);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function captureOutput(write, columns) {
|
|
103
|
+
return {
|
|
104
|
+
columns: Math.min(columns ?? process.stdout.columns ?? 88, 100),
|
|
105
|
+
write,
|
|
106
|
+
};
|
|
64
107
|
}
|
|
65
108
|
|
|
66
109
|
export function parseOptions(argv) {
|