offgrid-ai 0.4.4 → 0.4.6
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/cli.mjs +49 -39
- package/src/profile-setup.mjs +25 -1
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -184,7 +184,7 @@ async function modelCommandCenter(catalog) {
|
|
|
184
184
|
try {
|
|
185
185
|
const action = await prompt.choice("What would you like to do?", [
|
|
186
186
|
{ value: "run", label: "Start chatting", hint: "Start a local model and open Pi" },
|
|
187
|
-
{ value: "setup", label: "Set up
|
|
187
|
+
{ value: "setup", label: "Set up", hint: "Configure settings, MTP, context window" },
|
|
188
188
|
{ value: "inspect", label: "See model details", hint: "Show advanced paths, ports, and flags" },
|
|
189
189
|
{ value: "benchmark", label: "Benchmark", hint: "Coming soon" },
|
|
190
190
|
{ value: "remove", label: "Remove a saved setup", hint: "Delete a model setup from offgrid-ai" },
|
|
@@ -259,10 +259,9 @@ async function printModelCatalog({ profiles, newModels, managedItems, drafters }
|
|
|
259
259
|
} else {
|
|
260
260
|
for (const profile of profiles) {
|
|
261
261
|
const running = runningProfilesNow.some((item) => item.id === profile.id);
|
|
262
|
-
const piConfigured = await hasPiModel(profile);
|
|
263
262
|
const fileMissing = isProfileFileMissing(profile);
|
|
264
263
|
const num = itemNumber((item) => item.type === "profile" && item.profile.id === profile.id);
|
|
265
|
-
console.log(profileCatalogCard(num, profile, { running,
|
|
264
|
+
console.log(profileCatalogCard(num, profile, { running, fileMissing, drafters }));
|
|
266
265
|
}
|
|
267
266
|
}
|
|
268
267
|
|
|
@@ -299,7 +298,7 @@ function isProfileFileMissing(profile) {
|
|
|
299
298
|
return !existsSync(profile.modelPath);
|
|
300
299
|
}
|
|
301
300
|
|
|
302
|
-
function profileCatalogCard(num, profile, { running,
|
|
301
|
+
function profileCatalogCard(num, profile, { running, fileMissing, drafters }) {
|
|
303
302
|
const backend = backendFor(profile.backend);
|
|
304
303
|
const caps = profile.capabilities ?? {};
|
|
305
304
|
let status;
|
|
@@ -312,42 +311,44 @@ function profileCatalogCard(num, profile, { running, piConfigured, fileMissing,
|
|
|
312
311
|
}
|
|
313
312
|
const border = fileMissing ? pc.red : running ? pc.green : pc.dim;
|
|
314
313
|
const mtpDrafter = profile.drafterPath
|
|
315
|
-
? pc.green("
|
|
314
|
+
? pc.green("MTP")
|
|
316
315
|
: (drafters ? matchDrafter(profile.modelPath, drafters) : null)
|
|
317
|
-
? pc.yellow("available
|
|
316
|
+
? pc.yellow("MTP available")
|
|
318
317
|
: (caps.architecture === "gemma4")
|
|
319
|
-
? pc.yellow("
|
|
318
|
+
? pc.yellow("MTP: needs drafter")
|
|
320
319
|
: null;
|
|
321
|
-
const
|
|
320
|
+
const ctxLabel = profile.flags?.ctxSize ? `${(profile.flags.ctxSize / 1000).toFixed(0)}k ctx` : null;
|
|
321
|
+
const capLabel = fileMissing ? pc.red("File not found") : humanCapabilitySummary(caps);
|
|
322
|
+
const detailParts = [capLabel];
|
|
323
|
+
if (mtpDrafter) detailParts.push(mtpDrafter);
|
|
324
|
+
if (ctxLabel) detailParts.push(ctxLabel);
|
|
325
|
+
const detailLine = detailParts.join(pc.dim(" · "));
|
|
326
|
+
return renderCard(`${num}. ${profile.label}`, renderRows([
|
|
322
327
|
["Status", status],
|
|
323
|
-
["
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
rows.push(["Pi", piConfigured ? pc.dim("synced") : pc.yellow("Needs sync")]);
|
|
327
|
-
rows.push(["Runs with", backend.label]);
|
|
328
|
-
return renderCard(`${num}. ${profile.label}`, renderRows(rows), { formatBorder: border });
|
|
328
|
+
["Details", detailLine],
|
|
329
|
+
["Runs with", backend.label],
|
|
330
|
+
]), { formatBorder: border });
|
|
329
331
|
}
|
|
330
332
|
|
|
331
333
|
function downloadedModelCard(num, model, caps, { mtpAvailable } = {}) {
|
|
332
|
-
const
|
|
333
|
-
?
|
|
334
|
-
: (caps.architecture === "gemma4"
|
|
335
|
-
?
|
|
336
|
-
:
|
|
334
|
+
const mtpLabel = mtpAvailable
|
|
335
|
+
? pc.green("MTP ✓")
|
|
336
|
+
: (caps.architecture === "gemma4")
|
|
337
|
+
? pc.yellow("MTP: needs drafter")
|
|
338
|
+
: null;
|
|
339
|
+
const detailParts = [humanCapabilitySummary(caps)];
|
|
340
|
+
if (mtpLabel) detailParts.push(mtpLabel);
|
|
341
|
+
detailParts.push(formatBytes(model.sizeBytes));
|
|
337
342
|
return renderCard(`${num}. ${model.label}`, renderRows([
|
|
338
343
|
["Status", pc.yellow("Needs setup")],
|
|
339
|
-
["
|
|
340
|
-
...mtpRow,
|
|
341
|
-
["Size", formatBytes(model.sizeBytes)],
|
|
344
|
+
["Details", detailParts.join(pc.dim(" · "))],
|
|
342
345
|
]), { formatBorder: pc.yellow });
|
|
343
346
|
}
|
|
344
347
|
|
|
345
348
|
function managedModelCard(num, model, backend) {
|
|
346
349
|
return renderCard(`${num}. ${model.label}`, renderRows([
|
|
347
350
|
["Status", pc.dim(`Via ${backend.label}`)],
|
|
348
|
-
["
|
|
349
|
-
["Model ID", model.id],
|
|
350
|
-
...(model.quant ? [["Size/type", model.quant]] : []),
|
|
351
|
+
["Details", [model.id, model.quant].filter(Boolean).join(pc.dim(" · "))],
|
|
351
352
|
]), { formatBorder: pc.dim });
|
|
352
353
|
}
|
|
353
354
|
|
|
@@ -392,7 +393,13 @@ async function handleCatalogAction(prompt, action, item) {
|
|
|
392
393
|
}
|
|
393
394
|
|
|
394
395
|
if (action === "setup") {
|
|
395
|
-
if (item.type === "profile")
|
|
396
|
+
if (item.type === "profile") {
|
|
397
|
+
const profile = await readProfile(item.profile.id);
|
|
398
|
+
const configured = await configureLocalProfile(prompt, profile);
|
|
399
|
+
if (!configured) return;
|
|
400
|
+
await saveProfile(configured);
|
|
401
|
+
return await syncPiConfig(configured);
|
|
402
|
+
}
|
|
396
403
|
if (item.type === "managed") {
|
|
397
404
|
const profile = createManagedProfile(item.model, item.backendId);
|
|
398
405
|
await saveProfile(profile);
|
|
@@ -427,7 +434,6 @@ async function handleCatalogAction(prompt, action, item) {
|
|
|
427
434
|
async function printProfileDetails(profile) {
|
|
428
435
|
const backend = backendFor(profile.backend);
|
|
429
436
|
const isManaged = backend.type === "managed-server";
|
|
430
|
-
const piConfigured = await hasPiModel(profile);
|
|
431
437
|
const running = await isProfileRunning(profile);
|
|
432
438
|
const fileMissing = !isManaged && isProfileFileMissing(profile);
|
|
433
439
|
const statusRow = fileMissing
|
|
@@ -438,14 +444,16 @@ async function printProfileDetails(profile) {
|
|
|
438
444
|
: (profile.capabilities?.architecture === "gemma4")
|
|
439
445
|
? pc.yellow("MTP available — download a drafter model to enable 2× speedup")
|
|
440
446
|
: null;
|
|
447
|
+
const detailParts = [fileMissing ? pc.red("File not found") : humanCapabilitySummary(profile.capabilities ?? {})];
|
|
448
|
+
if (mtpStatus) detailParts.push(mtpStatus);
|
|
449
|
+
const ctxLabel = profile.flags?.ctxSize ? `${(profile.flags.ctxSize / 1000).toFixed(0)}k ctx` : null;
|
|
450
|
+
if (ctxLabel) detailParts.push(ctxLabel);
|
|
441
451
|
const overviewRows = [
|
|
442
452
|
["Name", pc.bold(profile.label)],
|
|
443
453
|
["Status", statusRow],
|
|
444
|
-
["
|
|
445
|
-
["Pi", piConfigured ? pc.dim("synced") : pc.yellow("Needs sync")],
|
|
454
|
+
["Details", detailParts.join(pc.dim(" · "))],
|
|
446
455
|
["Server", fileMissing ? pc.red(profile.baseUrl) : profile.baseUrl],
|
|
447
456
|
];
|
|
448
|
-
if (mtpStatus) overviewRows.push(["MTP", mtpStatus]);
|
|
449
457
|
console.log("\n" + renderSection("Model overview", renderRows(overviewRows)));
|
|
450
458
|
|
|
451
459
|
const detailRows = [
|
|
@@ -479,18 +487,21 @@ async function printProfileDetails(profile) {
|
|
|
479
487
|
function printGgufModelDetails(model, drafter) {
|
|
480
488
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
481
489
|
const mtpAvailable = caps.mtp || Boolean(drafter);
|
|
482
|
-
const
|
|
483
|
-
?
|
|
490
|
+
const mtpLabel = mtpAvailable
|
|
491
|
+
? pc.green("MTP ✓")
|
|
484
492
|
: (caps.architecture === "gemma4")
|
|
485
|
-
?
|
|
486
|
-
:
|
|
493
|
+
? pc.yellow("MTP: needs drafter")
|
|
494
|
+
: null;
|
|
495
|
+
const detailParts = [humanCapabilitySummary(caps)];
|
|
496
|
+
if (mtpLabel) detailParts.push(mtpLabel);
|
|
497
|
+
const ctxLabel = caps.ctxSize ? `${(caps.ctxSize / 1000).toFixed(0)}k ctx` : null;
|
|
498
|
+
if (ctxLabel) detailParts.push(ctxLabel);
|
|
499
|
+
detailParts.push(formatBytes(model.sizeBytes));
|
|
487
500
|
const overviewRows = [
|
|
488
501
|
["Name", pc.bold(model.label)],
|
|
489
502
|
["Status", pc.yellow("Needs one-time setup")],
|
|
490
|
-
["
|
|
503
|
+
["Details", detailParts.join(pc.dim(" · "))],
|
|
491
504
|
];
|
|
492
|
-
if (mtpRow.length) overviewRows.push(...mtpRow);
|
|
493
|
-
overviewRows.push(["Size", formatBytes(model.sizeBytes)]);
|
|
494
505
|
console.log("\n" + renderSection("Downloaded model", renderRows(overviewRows)));
|
|
495
506
|
const detailRows = [
|
|
496
507
|
["Local file", model.path],
|
|
@@ -499,8 +510,7 @@ function printGgufModelDetails(model, drafter) {
|
|
|
499
510
|
["Quant", model.quant ?? "unknown"],
|
|
500
511
|
];
|
|
501
512
|
if (drafter) {
|
|
502
|
-
detailRows.push(["Drafter", drafter.path]);
|
|
503
|
-
detailRows.push(["Drafter size", formatBytes(drafter.sizeBytes)]);
|
|
513
|
+
detailRows.push(["Drafter", drafter.path], ["Drafter size", formatBytes(drafter.sizeBytes)]);
|
|
504
514
|
}
|
|
505
515
|
console.log("\n" + renderSection("Model details", renderRows(detailRows), { columns: 110 }));
|
|
506
516
|
}
|
package/src/profile-setup.mjs
CHANGED
|
@@ -4,6 +4,9 @@ import { estimateMemory } from "./estimate.mjs";
|
|
|
4
4
|
import { findLlamaServer } from "./config.mjs";
|
|
5
5
|
import { baseUrlForFlags, LLAMA_CPP_PORT, LLAMA_CPP_MTP_PORT } from "./backends.mjs";
|
|
6
6
|
import { pc, formatBytes, renderRows, renderSection } from "./ui.mjs";
|
|
7
|
+
import { detectCapabilities } from "./autodetect.mjs";
|
|
8
|
+
import { matchDrafter } from "./scan.mjs";
|
|
9
|
+
import { scanGgufModels } from "./scan.mjs";
|
|
7
10
|
|
|
8
11
|
const execFileAsync = promisify(execFile);
|
|
9
12
|
|
|
@@ -29,7 +32,28 @@ const THINKING_DEFAULTS = {
|
|
|
29
32
|
|
|
30
33
|
export async function configureLocalProfile(prompt, profile) {
|
|
31
34
|
let configured = profile;
|
|
32
|
-
|
|
35
|
+
// Re-detect capabilities from the model file and check for drafters
|
|
36
|
+
// so that re-setup can pick up MTP availability, vision changes, etc.
|
|
37
|
+
const freshCaps = detectCapabilities(profile.modelPath, profile.mmprojPath);
|
|
38
|
+
let drafterPath = profile.drafterPath ?? null;
|
|
39
|
+
if (!drafterPath) {
|
|
40
|
+
const { drafters } = await scanGgufModels();
|
|
41
|
+
const drafter = matchDrafter(profile.modelPath, drafters);
|
|
42
|
+
if (drafter) drafterPath = drafter.path;
|
|
43
|
+
}
|
|
44
|
+
const hasMtp = freshCaps.mtp || Boolean(drafterPath);
|
|
45
|
+
const caps = { ...freshCaps, mtp: hasMtp };
|
|
46
|
+
// If MTP is newly available, switch backend and add drafter path
|
|
47
|
+
if (hasMtp && configured.backend !== "llama-cpp-mtp") {
|
|
48
|
+
configured = { ...configured, backend: "llama-cpp-mtp", providerId: "llama-cpp-mtp", drafterPath, capabilities: { ...configured.capabilities, mtp: true } };
|
|
49
|
+
}
|
|
50
|
+
if (drafterPath && !configured.drafterPath) {
|
|
51
|
+
configured = { ...configured, drafterPath };
|
|
52
|
+
}
|
|
53
|
+
// If vision was previously disabled but mmproj is back, re-enable
|
|
54
|
+
if (configured.disabledMmprojPath && configured.mmprojPath === null && freshCaps.vision) {
|
|
55
|
+
configured = { ...configured, mmprojPath: configured.disabledMmprojPath, disabledMmprojPath: undefined, capabilities: { ...configured.capabilities, vision: true, visionDisabledReason: undefined } };
|
|
56
|
+
}
|
|
33
57
|
|
|
34
58
|
console.log("");
|
|
35
59
|
console.log(renderSection("Model setup", renderRows([
|