offgrid-ai 0.3.26 → 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 +34 -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
|
}
|
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