offgrid-ai 0.3.25 → 0.3.27
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/autodetect.mjs +9 -2
- package/src/cli.mjs +47 -19
- package/src/postinstall.mjs +12 -0
- package/src/profile-setup.mjs +34 -1
- package/src/profiles.mjs +1 -0
- package/src/scan.mjs +1 -1
package/package.json
CHANGED
package/src/autodetect.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { readGgufMetadata } from "./gguf.mjs";
|
|
|
6
6
|
|
|
7
7
|
export function detectCapabilities(modelPath, mmprojPath) {
|
|
8
8
|
const meta = safeReadGgufMetadata(modelPath);
|
|
9
|
+
const mmprojMeta = mmprojPath ? safeReadGgufMetadata(mmprojPath) : {};
|
|
9
10
|
const name = basename(modelPath).toLowerCase();
|
|
10
11
|
const pathHints = String(modelPath).toLowerCase();
|
|
11
12
|
|
|
@@ -25,13 +26,14 @@ export function detectCapabilities(modelPath, mmprojPath) {
|
|
|
25
26
|
|
|
26
27
|
// Vision — mmproj present
|
|
27
28
|
const vision = Boolean(mmprojPath && existsSync(mmprojPath));
|
|
29
|
+
const mmprojProjectorType = stringMeta(mmprojMeta, "clip.vision.projector_type") ?? stringMeta(mmprojMeta, "clip.audio.projector_type") ?? null;
|
|
28
30
|
|
|
29
31
|
// MTP (multi-token prediction) — detect speculative decoding.
|
|
30
32
|
// Do not treat all Qwen models as MTP; require an explicit filename or metadata hint.
|
|
31
33
|
const mtp = /\bmtp\b|draft-mtp|multi-token/i.test(pathHints) || Object.keys(meta).some((key) => /mtp|draft|speculative/i.test(key));
|
|
32
34
|
|
|
33
35
|
// Quantization
|
|
34
|
-
const quant = name.match(/(Q\d_K_[A-Z]+|UD-[A-Z0-9_]+)/i)?.[1] ?? null;
|
|
36
|
+
const quant = name.match(/(Q\d_K_[A-Z]+|Q\d_[01]|UD-[A-Z0-9_]+)/i)?.[1] ?? null;
|
|
35
37
|
|
|
36
38
|
// Context size from metadata, fallback to name hints
|
|
37
39
|
const metaCtx = architecture
|
|
@@ -39,7 +41,7 @@ export function detectCapabilities(modelPath, mmprojPath) {
|
|
|
39
41
|
: undefined;
|
|
40
42
|
const ctxSize = metaCtx ?? (thinking ? 80000 : 32768);
|
|
41
43
|
|
|
42
|
-
return { architecture, thinking, vision, mtp, qat, imatrix, quant, metaCtx, ctxSize, meta };
|
|
44
|
+
return { architecture, thinking, vision, mtp, qat, imatrix, quant, metaCtx, ctxSize, meta, mmprojProjectorType };
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
// ── Compute llama-server flags from capabilities ───────────────────────────
|
|
@@ -127,4 +129,9 @@ function safeReadGgufMetadata(modelPath) {
|
|
|
127
129
|
function numberMeta(meta, key) {
|
|
128
130
|
const value = key ? meta[key] : undefined;
|
|
129
131
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stringMeta(meta, key) {
|
|
135
|
+
const value = key ? meta[key] : undefined;
|
|
136
|
+
return typeof value === "string" && value ? value : undefined;
|
|
130
137
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -456,6 +456,33 @@ function capabilitySummary(caps) {
|
|
|
456
456
|
return parts.length > 0 ? parts.join(" · ") : "standard GGUF";
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
function isUnsupportedMmprojError(err, profile) {
|
|
460
|
+
const message = String(err?.message ?? "");
|
|
461
|
+
return Boolean(profile.mmprojPath && /unknown projector type|failed to load multimodal model|failed to load CLIP model/i.test(message));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function textOnlyProfile(profile) {
|
|
465
|
+
return normalizeProfile({
|
|
466
|
+
...profile,
|
|
467
|
+
mmprojPath: null,
|
|
468
|
+
disabledMmprojPath: profile.disabledMmprojPath ?? profile.mmprojPath,
|
|
469
|
+
capabilities: { ...(profile.capabilities ?? {}), vision: false, visionDisabledReason: "unsupported-mmproj" },
|
|
470
|
+
commandArgv: removeCommandOption(profile.commandArgv ?? [], "--mmproj"),
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function removeCommandOption(argv, flag) {
|
|
475
|
+
const next = [];
|
|
476
|
+
for (let i = 0; i < argv.length; i++) {
|
|
477
|
+
if (argv[i] === flag) {
|
|
478
|
+
if (argv[i + 1] && !argv[i + 1].startsWith("--")) i += 1;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
next.push(argv[i]);
|
|
482
|
+
}
|
|
483
|
+
return next;
|
|
484
|
+
}
|
|
485
|
+
|
|
459
486
|
function createManagedProfile(model, backendId) {
|
|
460
487
|
return normalizeProfile({
|
|
461
488
|
id: model.id.replace(/[^a-z0-9._-]+/gi, "-").toLowerCase(),
|
|
@@ -510,6 +537,13 @@ async function runProfile(profile, options = {}) {
|
|
|
510
537
|
if (state?.pid) {
|
|
511
538
|
try { await stopProfile(profile); } catch { /* best effort */ }
|
|
512
539
|
}
|
|
540
|
+
if (!options.textOnlyRetry && isUnsupportedMmprojError(err, profile)) {
|
|
541
|
+
console.log(pc.yellow("Vision projector is not supported by this llama.cpp build. Retrying text-only."));
|
|
542
|
+
console.log(pc.dim("Update llama.cpp later to re-enable vision for this model."));
|
|
543
|
+
const textOnly = textOnlyProfile(profile);
|
|
544
|
+
await saveProfile(textOnly);
|
|
545
|
+
return await runProfile(textOnly, { ...options, textOnlyRetry: true });
|
|
546
|
+
}
|
|
513
547
|
throw err;
|
|
514
548
|
}
|
|
515
549
|
}
|
|
@@ -965,29 +999,23 @@ async function uninstallCommand(argv) {
|
|
|
965
999
|
console.log(pc.green("All servers stopped."));
|
|
966
1000
|
}
|
|
967
1001
|
|
|
968
|
-
// Ask about data
|
|
969
1002
|
const dataDir = DATA_DIR;
|
|
970
|
-
const
|
|
1003
|
+
const mode = await prompt.choice("Choose uninstall type", [
|
|
1004
|
+
{ value: "keep-data", label: "Uninstall app only", hint: `keep profiles and settings in ${dataDir}` },
|
|
1005
|
+
{ value: "delete-data", label: "Full uninstall", hint: "delete profiles/settings, then uninstall app" },
|
|
1006
|
+
{ value: "cancel", label: "Cancel" },
|
|
1007
|
+
], "keep-data");
|
|
971
1008
|
|
|
972
|
-
if (
|
|
973
|
-
const confirmDelete = await prompt.yesNo(`Delete ${dataDir}? This removes all profiles and settings.`, false);
|
|
974
|
-
if (confirmDelete) {
|
|
975
|
-
await removeDataDir();
|
|
976
|
-
} else {
|
|
977
|
-
console.log(pc.dim("Keeping data directory."));
|
|
978
|
-
}
|
|
979
|
-
} else {
|
|
980
|
-
console.log(pc.dim(`Keeping ${dataDir} for when you reinstall.`));
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// Remove the npm package
|
|
984
|
-
const confirmUninstall = await prompt.yesNo("Uninstall offgrid-ai npm package?", true);
|
|
985
|
-
if (confirmUninstall) {
|
|
986
|
-
await removeShellPath();
|
|
987
|
-
await removeSelf();
|
|
988
|
-
} else {
|
|
1009
|
+
if (mode === "cancel") {
|
|
989
1010
|
console.log(pc.dim("Cancelled."));
|
|
1011
|
+
return;
|
|
990
1012
|
}
|
|
1013
|
+
|
|
1014
|
+
if (mode === "delete-data") await removeDataDir();
|
|
1015
|
+
else console.log(pc.dim(`Keeping ${dataDir} for when you reinstall.`));
|
|
1016
|
+
|
|
1017
|
+
await removeShellPath();
|
|
1018
|
+
await removeSelf();
|
|
991
1019
|
} finally {
|
|
992
1020
|
prompt.close();
|
|
993
1021
|
}
|
package/src/postinstall.mjs
CHANGED
|
@@ -8,6 +8,12 @@ if (process.env.npm_config_global !== "true") process.exit(0);
|
|
|
8
8
|
const prefix = process.env.npm_config_prefix;
|
|
9
9
|
if (!prefix) process.exit(0);
|
|
10
10
|
|
|
11
|
+
if (isHermesPrefix(prefix, process.env.HOME)) {
|
|
12
|
+
console.log("offgrid-ai installed with a Hermes-managed npm prefix.");
|
|
13
|
+
console.log("Not adding Hermes Node to PATH automatically. Use your normal Node/npm, or run the offgrid-ai install script.");
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
const npmBin = join(prefix, "bin");
|
|
12
18
|
const marker = "# Added by offgrid-ai installer";
|
|
13
19
|
const pathLine = `export PATH="${npmBin}:$PATH"`;
|
|
@@ -39,3 +45,9 @@ if (!content.includes(npmBin)) {
|
|
|
39
45
|
console.log(`offgrid-ai is installed in ${npmBin}`);
|
|
40
46
|
console.log(`Open a new terminal if the command is not found yet.`);
|
|
41
47
|
}
|
|
48
|
+
|
|
49
|
+
function isHermesPrefix(prefix, home) {
|
|
50
|
+
const normalized = prefix.replace(/\\/gu, "/");
|
|
51
|
+
if (normalized.includes("/.hermes/")) return true;
|
|
52
|
+
return Boolean(home && normalized === `${home.replace(/\\/gu, "/")}/.hermes/node`);
|
|
53
|
+
}
|
package/src/profile-setup.mjs
CHANGED
|
@@ -50,10 +50,22 @@ export async function configureLocalProfile(prompt, profile) {
|
|
|
50
50
|
console.log("");
|
|
51
51
|
console.log(renderSection("QAT detected", renderRows([
|
|
52
52
|
["Meaning", "quantization-aware trained"],
|
|
53
|
-
["Runtime flags", "none
|
|
53
|
+
["Runtime flags", "none QAT-specific"],
|
|
54
54
|
])));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
if (caps.vision && profile.mmprojPath) {
|
|
58
|
+
console.log("");
|
|
59
|
+
const gemma4Unified = isGemma4UnifiedProjector(caps.mmprojProjectorType);
|
|
60
|
+
console.log(renderSection("Vision projector detected", renderRows([
|
|
61
|
+
["Projector", caps.mmprojProjectorType ?? "unknown"],
|
|
62
|
+
["Flag", `--mmproj ${profile.mmprojPath}`],
|
|
63
|
+
...(gemma4Unified ? [["Note", pc.yellow("Gemma 4 unified projectors need newer llama.cpp than current Homebrew stable.")]] : []),
|
|
64
|
+
])));
|
|
65
|
+
const useVision = await prompt.yesNo("Enable vision with --mmproj?", !gemma4Unified);
|
|
66
|
+
configured = useVision ? applyVisionDefaults(configured) : removeVisionDefaults(configured, gemma4Unified ? "gemma4-unified-unsupported" : "user-disabled");
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
if (caps.thinking) {
|
|
58
70
|
console.log("");
|
|
59
71
|
console.log(renderSection("Thinking model detected", renderRows([
|
|
@@ -106,6 +118,23 @@ function removeMtpDefaults(profile) {
|
|
|
106
118
|
});
|
|
107
119
|
}
|
|
108
120
|
|
|
121
|
+
function applyVisionDefaults(profile) {
|
|
122
|
+
if (!profile.mmprojPath) return profile;
|
|
123
|
+
return applyProfileFlags({
|
|
124
|
+
...profile,
|
|
125
|
+
capabilities: { ...(profile.capabilities ?? {}), vision: true, visionDisabledReason: undefined },
|
|
126
|
+
}, profile.flags, { values: { "--mmproj": profile.mmprojPath } });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function removeVisionDefaults(profile, reason) {
|
|
130
|
+
return applyProfileFlags({
|
|
131
|
+
...profile,
|
|
132
|
+
disabledMmprojPath: profile.mmprojPath,
|
|
133
|
+
mmprojPath: null,
|
|
134
|
+
capabilities: { ...(profile.capabilities ?? {}), vision: false, visionDisabledReason: reason },
|
|
135
|
+
}, profile.flags, { remove: ["--mmproj"] });
|
|
136
|
+
}
|
|
137
|
+
|
|
109
138
|
function applyThinkingDefaults(profile) {
|
|
110
139
|
const flags = { ...profile.flags, ...THINKING_DEFAULTS };
|
|
111
140
|
return applyProfileFlags(profile, flags);
|
|
@@ -179,6 +208,10 @@ function renderMemoryEstimate(profile) {
|
|
|
179
208
|
}
|
|
180
209
|
}
|
|
181
210
|
|
|
211
|
+
function isGemma4UnifiedProjector(projectorType) {
|
|
212
|
+
return /gemma4u[va]/i.test(String(projectorType ?? ""));
|
|
213
|
+
}
|
|
214
|
+
|
|
182
215
|
function detectionSummary(caps) {
|
|
183
216
|
const parts = [];
|
|
184
217
|
if (caps.architecture) parts.push(caps.architecture);
|
package/src/profiles.mjs
CHANGED
package/src/scan.mjs
CHANGED