offgrid-ai 0.6.5 → 0.6.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/autodetect.mjs +7 -8
- package/src/backends.mjs +20 -10
- package/src/cli.mjs +10 -31
- package/src/profile-setup.mjs +5 -7
- package/src/profiles.mjs +6 -13
- package/src/scan.mjs +2 -21
package/package.json
CHANGED
package/src/autodetect.mjs
CHANGED
|
@@ -73,14 +73,13 @@ export function computeFlags(capabilities, modelPath, mmprojPath, draftModelPath
|
|
|
73
73
|
flags.chatTemplateKwargs = { enable_thinking: true };
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// Build argv
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
if (draftModelPath && !hfRef) argv.push("--spec-draft-model", draftModelPath);
|
|
76
|
+
// Build argv
|
|
77
|
+
const argv = [
|
|
78
|
+
"--model", modelPath,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
if (mmprojPath) argv.push("--mmproj", mmprojPath);
|
|
82
|
+
if (draftModelPath) argv.push("--spec-draft-model", draftModelPath);
|
|
84
83
|
|
|
85
84
|
argv.push(
|
|
86
85
|
"--host", String(flags.host),
|
package/src/backends.mjs
CHANGED
|
@@ -117,16 +117,18 @@ async function scanOmlxModels() {
|
|
|
117
117
|
if (!response.ok) return [];
|
|
118
118
|
const body = await response.json();
|
|
119
119
|
if (!Array.isArray(body?.data)) return [];
|
|
120
|
-
return body.data
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
return body.data
|
|
121
|
+
.filter((model) => isChatOmlxModel(model))
|
|
122
|
+
.map((model) => ({
|
|
123
|
+
id: model.id,
|
|
124
|
+
label: omlxLabel(model.id),
|
|
125
|
+
aliasSuggestion: model.id,
|
|
126
|
+
sizeBytes: 0,
|
|
127
|
+
quant: null,
|
|
128
|
+
family: null,
|
|
129
|
+
backend: "omlx",
|
|
130
|
+
source: "omlx",
|
|
131
|
+
})).sort((a, b) => a.label.localeCompare(b.label));
|
|
130
132
|
} catch {
|
|
131
133
|
return [];
|
|
132
134
|
}
|
|
@@ -141,6 +143,14 @@ function isLocalOllamaModel(model) {
|
|
|
141
143
|
return true;
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
function isChatOmlxModel(model) {
|
|
147
|
+
if (typeof model?.id !== "string" || !model.id.trim()) return false;
|
|
148
|
+
const type = String(model.type ?? model.model_type ?? "").toLowerCase();
|
|
149
|
+
if (["embedding", "embeddings", "reranker", "tool", "converter"].includes(type)) return false;
|
|
150
|
+
if (Object.hasOwn(model, "max_model_len") && model.max_model_len === null) return false;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
144
154
|
function ollamaLabel(name) {
|
|
145
155
|
return name.replace(/[-_]/g, " ").replace(/^gemma\b/i, "Gemma").replace(/^qwen/i, "Qwen");
|
|
146
156
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -284,7 +284,7 @@ function optionTag(text, color, width) {
|
|
|
284
284
|
function optionStatusTag(kind) {
|
|
285
285
|
const statuses = {
|
|
286
286
|
running: ["RUNNING", pc.green],
|
|
287
|
-
ready: ["
|
|
287
|
+
ready: ["READY", pc.green],
|
|
288
288
|
missing: ["FILE MISSING", pc.red],
|
|
289
289
|
setup: ["NEEDS SETUP", pc.yellow],
|
|
290
290
|
};
|
|
@@ -299,7 +299,6 @@ function optionSourceTag(sourceId, label) {
|
|
|
299
299
|
ollama: pc.green,
|
|
300
300
|
omlx: pc.magenta,
|
|
301
301
|
gguf: pc.cyan,
|
|
302
|
-
hf: pc.cyan,
|
|
303
302
|
};
|
|
304
303
|
return optionTag(label, colors[sourceId] ?? pc.dim, OPTION_SOURCE_WIDTH);
|
|
305
304
|
}
|
|
@@ -308,11 +307,7 @@ function optionLabel({ status, source, name, details = [] }) {
|
|
|
308
307
|
return [status, source, pc.bold(name), ...details].filter(Boolean).join(OPTION_SEPARATOR);
|
|
309
308
|
}
|
|
310
309
|
|
|
311
|
-
function
|
|
312
|
-
return parts.filter(Boolean).join(" · ");
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function modelSelectOption(item, { runningProfilesNow, drafters }) {
|
|
310
|
+
function modelSelectOption(item, { runningProfilesNow }) {
|
|
316
311
|
if (item.type === "profile") {
|
|
317
312
|
const { profile } = item;
|
|
318
313
|
const backend = backendFor(profile.backend);
|
|
@@ -320,41 +315,27 @@ function modelSelectOption(item, { runningProfilesNow, drafters }) {
|
|
|
320
315
|
const fileMissing = item.fileMissing;
|
|
321
316
|
const status = optionStatusTag(fileMissing ? "missing" : running ? "running" : "ready");
|
|
322
317
|
const source = optionSourceTag(profile.backend, backend.label);
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
if (profile.drafterPath) mtpLabel = pc.green("MTP on");
|
|
326
|
-
else if (drafters ? matchDrafter(profile.modelPath, drafters) : null) mtpLabel = pc.yellow("MTP available");
|
|
327
|
-
else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP needs drafter");
|
|
328
|
-
const ctxLabel = profile.flags?.ctxSize ? pc.dim(`${(profile.flags.ctxSize / 1000).toFixed(0)}k ctx`) : null;
|
|
329
|
-
const label = optionLabel({ status, source, name: profile.label, details: [ctxLabel, mtpLabel] });
|
|
330
|
-
return { value: itemKey(item), label, hint: optionHint([profile.modelAlias, humanCapabilitySummary(caps), profile.baseUrl]) };
|
|
318
|
+
const label = optionLabel({ status, source, name: profile.label });
|
|
319
|
+
return { value: itemKey(item), label };
|
|
331
320
|
}
|
|
332
321
|
if (item.type === "new") {
|
|
333
|
-
const { model
|
|
334
|
-
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
335
|
-
const mtpAvailable = caps.mtp || Boolean(drafter) || Boolean(model.hfRepo && /gemma-?4/i.test(`${model.hfRepo} ${model.label}`));
|
|
336
|
-
let mtpLabel;
|
|
337
|
-
if (mtpAvailable) mtpLabel = pc.green("MTP available");
|
|
338
|
-
else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP needs drafter");
|
|
322
|
+
const { model } = item;
|
|
339
323
|
const label = optionLabel({
|
|
340
324
|
status: optionStatusTag("setup"),
|
|
341
|
-
source: optionSourceTag(
|
|
325
|
+
source: optionSourceTag("gguf", "GGUF file"),
|
|
342
326
|
name: model.label,
|
|
343
|
-
details: [pc.dim(formatBytes(model.sizeBytes)), mtpLabel],
|
|
344
327
|
});
|
|
345
|
-
return { value: itemKey(item), label
|
|
328
|
+
return { value: itemKey(item), label };
|
|
346
329
|
}
|
|
347
330
|
// managed
|
|
348
331
|
const { model, backendId } = item;
|
|
349
332
|
const backend = BACKENDS[backendId];
|
|
350
|
-
const details = [model.quant ? pc.dim(model.quant) : null, model.sizeBytes ? pc.dim(formatBytes(model.sizeBytes)) : null];
|
|
351
333
|
const label = optionLabel({
|
|
352
334
|
status: optionStatusTag("setup"),
|
|
353
335
|
source: optionSourceTag(backendId, backend.label),
|
|
354
336
|
name: model.label,
|
|
355
|
-
details,
|
|
356
337
|
});
|
|
357
|
-
return { value: itemKey(item), label
|
|
338
|
+
return { value: itemKey(item), label };
|
|
358
339
|
}
|
|
359
340
|
|
|
360
341
|
function actionsForItem(item) {
|
|
@@ -435,7 +416,7 @@ function printModelCards(items, { runningProfilesNow, drafters }) {
|
|
|
435
416
|
for (const item of newModels) {
|
|
436
417
|
const { model, drafter } = item;
|
|
437
418
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
438
|
-
const mtpAvailable = caps.mtp || Boolean(drafter)
|
|
419
|
+
const mtpAvailable = caps.mtp || Boolean(drafter);
|
|
439
420
|
const mtpLabel = mtpAvailable
|
|
440
421
|
? pc.green("MTP ✓")
|
|
441
422
|
: (caps.architecture === "gemma4")
|
|
@@ -543,7 +524,6 @@ async function printProfileDetails(profile) {
|
|
|
543
524
|
];
|
|
544
525
|
if (!isManaged) {
|
|
545
526
|
detailRows.push(
|
|
546
|
-
...(profile.hfRepo ? [["Hugging Face", `${profile.hfRepo}${profile.hfVariant ? `:${profile.hfVariant}` : ""}`]] : []),
|
|
547
527
|
["Local file", fileMissing ? pc.red(`${profile.modelPath} (not found)`) : profile.modelPath ?? "unknown"],
|
|
548
528
|
["Vision file", profile.mmprojPath ? (existsSync(profile.mmprojPath) ? profile.mmprojPath : pc.red(`${profile.mmprojPath} (not found)`)) : "none"],
|
|
549
529
|
["Model size", profile.modelPath && existsSync(profile.modelPath) ? formatBytes(statSync(profile.modelPath).size) : "unknown"],
|
|
@@ -566,7 +546,7 @@ async function printProfileDetails(profile) {
|
|
|
566
546
|
|
|
567
547
|
function printGgufModelDetails(model, drafter) {
|
|
568
548
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
569
|
-
const mtpAvailable = caps.mtp || Boolean(drafter)
|
|
549
|
+
const mtpAvailable = caps.mtp || Boolean(drafter);
|
|
570
550
|
const mtpLabel = mtpAvailable
|
|
571
551
|
? pc.green("MTP ✓")
|
|
572
552
|
: (caps.architecture === "gemma4")
|
|
@@ -584,7 +564,6 @@ function printGgufModelDetails(model, drafter) {
|
|
|
584
564
|
];
|
|
585
565
|
console.log("\n" + renderSection("Downloaded model", renderRows(overviewRows)));
|
|
586
566
|
const detailRows = [
|
|
587
|
-
...(model.hfRepo ? [["Hugging Face", `${model.hfRepo}${model.hfVariant ? `:${model.hfVariant}` : ""}`]] : []),
|
|
588
567
|
["Local file", model.path],
|
|
589
568
|
["Vision file", model.mmprojPath ?? "none"],
|
|
590
569
|
["Detected", capabilitySummary(caps)],
|
package/src/profile-setup.mjs
CHANGED
|
@@ -34,16 +34,14 @@ export async function configureLocalProfile(prompt, profile) {
|
|
|
34
34
|
let configured = profile;
|
|
35
35
|
// Re-detect capabilities from the model file and check for drafters
|
|
36
36
|
// so that re-setup can pick up MTP availability, vision changes, etc.
|
|
37
|
-
const
|
|
38
|
-
const freshCaps = { ...detectCapabilities(profile.modelPath, profile.mmprojPath), ...hfSource };
|
|
37
|
+
const freshCaps = detectCapabilities(profile.modelPath, profile.mmprojPath);
|
|
39
38
|
let drafterPath = profile.drafterPath ?? null;
|
|
40
|
-
if (!drafterPath
|
|
39
|
+
if (!drafterPath) {
|
|
41
40
|
const { drafters } = await scanGgufModels();
|
|
42
41
|
const drafter = matchDrafter(profile.modelPath, drafters);
|
|
43
42
|
if (drafter) drafterPath = drafter.path;
|
|
44
43
|
}
|
|
45
|
-
const
|
|
46
|
-
const hasMtp = freshCaps.mtp || Boolean(drafterPath) || hfGemma4Mtp;
|
|
44
|
+
const hasMtp = freshCaps.mtp || Boolean(drafterPath);
|
|
47
45
|
const caps = { ...freshCaps, mtp: hasMtp };
|
|
48
46
|
// If MTP is newly available, switch backend and add drafter path
|
|
49
47
|
if (hasMtp && configured.backend !== "llama-cpp-mtp") {
|
|
@@ -73,7 +71,7 @@ export async function configureLocalProfile(prompt, profile) {
|
|
|
73
71
|
console.log(renderSection("MTP detected", renderRows([
|
|
74
72
|
["Backend", "llama.cpp MTP"],
|
|
75
73
|
["Port", String(LLAMA_CPP_MTP_PORT)],
|
|
76
|
-
["Flags", `--spec-type draft-mtp --spec-draft-n-max 4${configured.drafterPath
|
|
74
|
+
["Flags", `--spec-type draft-mtp --spec-draft-n-max 4${configured.drafterPath ? " --spec-draft-model <drafter>" : ""}`],
|
|
77
75
|
])));
|
|
78
76
|
if (drafterInfo) console.log(pc.dim(drafterInfo));
|
|
79
77
|
const useMtp = await prompt.yesNo("Use MTP speculative decoding?", true);
|
|
@@ -144,7 +142,7 @@ function applyMtpDefaults(profile) {
|
|
|
144
142
|
const edits = {
|
|
145
143
|
values: { "--spec-type": "draft-mtp", "--spec-draft-n-max": 4 },
|
|
146
144
|
};
|
|
147
|
-
if (profile.drafterPath
|
|
145
|
+
if (profile.drafterPath) {
|
|
148
146
|
edits.values["--spec-draft-model"] = profile.drafterPath;
|
|
149
147
|
}
|
|
150
148
|
return applyProfileFlags({ ...profile, backend: "llama-cpp-mtp", providerId: "llama-cpp-mtp" }, flags, edits);
|
package/src/profiles.mjs
CHANGED
|
@@ -134,15 +134,11 @@ export function normalizeProfile(profile) {
|
|
|
134
134
|
export async function createProfileFromModel(model, backendId, drafterPath) {
|
|
135
135
|
const { detectCapabilities } = await import("./autodetect.mjs");
|
|
136
136
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
137
|
-
// If a drafter is provided, this model supports MTP regardless of filename
|
|
138
|
-
|
|
139
|
-
// discovery so root mtp-*.gguf drafters do not need offgrid-ai file matching.
|
|
140
|
-
const hfGemma4Mtp = Boolean(model.hfRepo && /gemma-?4/i.test(`${model.hfRepo} ${model.label}`));
|
|
141
|
-
const hasMtp = caps.mtp || Boolean(drafterPath) || hfGemma4Mtp;
|
|
137
|
+
// If a drafter is provided, this model supports MTP regardless of filename
|
|
138
|
+
const hasMtp = caps.mtp || Boolean(drafterPath);
|
|
142
139
|
const backend = backendId ?? (hasMtp ? "llama-cpp-mtp" : "llama-cpp");
|
|
143
|
-
const hfSource = model.hfRepo ? { hfRepo: model.hfRepo, hfVariant: model.hfVariant } : {};
|
|
144
140
|
const { flags, argv } = computeFlags(
|
|
145
|
-
{ ...caps, mtp: hasMtp
|
|
141
|
+
{ ...caps, mtp: hasMtp },
|
|
146
142
|
model.path,
|
|
147
143
|
model.mmprojPath,
|
|
148
144
|
drafterPath ?? null,
|
|
@@ -154,12 +150,11 @@ export async function createProfileFromModel(model, backendId, drafterPath) {
|
|
|
154
150
|
backend,
|
|
155
151
|
providerId: backend,
|
|
156
152
|
modelAlias: model.aliasSuggestion,
|
|
157
|
-
source: model.
|
|
153
|
+
source: model.source,
|
|
158
154
|
modelPath: model.path,
|
|
159
|
-
mmprojPath: model.
|
|
160
|
-
...hfSource,
|
|
155
|
+
mmprojPath: model.mmprojPath,
|
|
161
156
|
drafterPath: drafterPath ?? null,
|
|
162
|
-
capabilities: summarizeCapabilities({ ...caps, mtp: hasMtp
|
|
157
|
+
capabilities: summarizeCapabilities({ ...caps, mtp: hasMtp }),
|
|
163
158
|
preset: null, // no presets — auto-detected
|
|
164
159
|
flags,
|
|
165
160
|
commandArgv: argv,
|
|
@@ -177,8 +172,6 @@ function summarizeCapabilities(caps) {
|
|
|
177
172
|
quant: caps.quant,
|
|
178
173
|
metaCtx: caps.metaCtx,
|
|
179
174
|
mmprojProjectorType: caps.mmprojProjectorType,
|
|
180
|
-
hfRepo: caps.hfRepo,
|
|
181
|
-
hfVariant: caps.hfVariant,
|
|
182
175
|
ctxSize: caps.ctxSize,
|
|
183
176
|
};
|
|
184
177
|
}
|
package/src/scan.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, join
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { getModelScanDirs } from "./config.mjs";
|
|
5
5
|
import { readGgufMetadata } from "./gguf.mjs";
|
|
6
6
|
|
|
@@ -67,7 +67,6 @@ async function scanOneDir(root) {
|
|
|
67
67
|
source: "local-gguf",
|
|
68
68
|
});
|
|
69
69
|
} else {
|
|
70
|
-
const hf = inferHfRef(path, name);
|
|
71
70
|
models.push({
|
|
72
71
|
path,
|
|
73
72
|
mmprojPath,
|
|
@@ -76,8 +75,7 @@ async function scanOneDir(root) {
|
|
|
76
75
|
quant: quantFromName(name),
|
|
77
76
|
sizeBytes,
|
|
78
77
|
backend: "llama-cpp",
|
|
79
|
-
source:
|
|
80
|
-
...(hf ? { hfRepo: hf.repo, hfVariant: hf.variant } : {}),
|
|
78
|
+
source: "local-gguf",
|
|
81
79
|
});
|
|
82
80
|
}
|
|
83
81
|
}
|
|
@@ -160,23 +158,6 @@ function quantFromName(name) {
|
|
|
160
158
|
return name.match(/(Q\d_K_[A-Z]+|Q\d_[01]|UD-[A-Z0-9_]+)/)?.[1];
|
|
161
159
|
}
|
|
162
160
|
|
|
163
|
-
export function inferHfRef(path, name = basename(path).replace(/\.gguf$/i, "")) {
|
|
164
|
-
const parts = path.split(sep);
|
|
165
|
-
const hubIndex = parts.lastIndexOf("hub");
|
|
166
|
-
const hfCacheRepo = hubIndex >= 0 ? parts.find((part, index) => index > hubIndex && part.startsWith("models--")) : null;
|
|
167
|
-
if (hfCacheRepo) {
|
|
168
|
-
return { repo: hfCacheRepo.replace(/^models--/u, "").replace(/--/gu, "/"), variant: quantFromName(name) ?? name };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const lmStudioIndex = parts.lastIndexOf(".lmstudio");
|
|
172
|
-
if (lmStudioIndex >= 0 && parts[lmStudioIndex + 1] === "models") {
|
|
173
|
-
const org = parts[lmStudioIndex + 2];
|
|
174
|
-
const repo = parts[lmStudioIndex + 3];
|
|
175
|
-
if (org && repo) return { repo: `${org}/${repo}`, variant: quantFromName(name) ?? name };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
161
|
|
|
181
162
|
function safeReadGgufMetadata(path) {
|
|
182
163
|
try {
|