offgrid-ai 0.6.3 → 0.6.5
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 +8 -7
- package/src/benchmark.mjs +7 -29
- package/src/cli.mjs +7 -4
- package/src/profile-setup.mjs +7 -5
- package/src/profiles.mjs +13 -5
- package/src/scan.mjs +22 -2
package/package.json
CHANGED
package/src/autodetect.mjs
CHANGED
|
@@ -73,13 +73,14 @@ 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 (
|
|
76
|
+
// Build argv. Prefer llama.cpp's Hugging Face resolver when a repo ref is
|
|
77
|
+
// known, so companion files such as Gemma 4 MTP drafters are discovered by
|
|
78
|
+
// llama.cpp instead of offgrid-ai maintaining repo-specific file rules.
|
|
79
|
+
const hfRef = capabilities.hfRepo ? `${capabilities.hfRepo}${capabilities.hfVariant ? `:${capabilities.hfVariant}` : ""}` : null;
|
|
80
|
+
const argv = hfRef ? ["-hf", hfRef] : ["--model", modelPath];
|
|
81
|
+
|
|
82
|
+
if (mmprojPath && !hfRef) argv.push("--mmproj", mmprojPath);
|
|
83
|
+
if (draftModelPath && !hfRef) argv.push("--spec-draft-model", draftModelPath);
|
|
83
84
|
|
|
84
85
|
argv.push(
|
|
85
86
|
"--host", String(flags.host),
|
package/src/benchmark.mjs
CHANGED
|
@@ -7,7 +7,6 @@ import { execFile } from "node:child_process";
|
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { ensureDirs, loadConfig, saveConfig } from "./config.mjs";
|
|
9
9
|
import { backendFor } from "./backends.mjs";
|
|
10
|
-
import { serverMatchesProfile, serverReady } from "./process.mjs";
|
|
11
10
|
import { pc, createPrompt, renderRows, renderSection } from "./ui.mjs";
|
|
12
11
|
|
|
13
12
|
const execFileAsync = promisify(execFile);
|
|
@@ -151,35 +150,15 @@ function intendedRunnerForProfile(profile) {
|
|
|
151
150
|
return harnessDisplayName(id);
|
|
152
151
|
}
|
|
153
152
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
if (!ready) return { running: false };
|
|
158
|
-
const match = await serverMatchesProfile(profile).catch(() => ({ matches: true, reason: "server responded" }));
|
|
159
|
-
return { running: match.matches, reason: match.reason };
|
|
160
|
-
}
|
|
153
|
+
function printBenchmarkNextSteps({ repoPath, runDirectory, profile, modelId, runnerLabel }) {
|
|
154
|
+
const runCommand = profile ? `offgrid-ai run ${profile.id}` : null;
|
|
155
|
+
const runnerCommand = runCommand ?? `Open ${runnerLabel} for ${modelId}`;
|
|
161
156
|
|
|
162
|
-
function printBenchmarkNextSteps({ repoPath, profile, modelId, runnerLabel, serverStatus }) {
|
|
163
157
|
console.log("");
|
|
164
158
|
console.log(pc.bold("Next steps"));
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const command = `offgrid-ai run ${profile.id}`;
|
|
169
|
-
if (serverStatus?.running) {
|
|
170
|
-
const reason = serverStatus.reason ? pc.dim(` (${serverStatus.reason})`) : "";
|
|
171
|
-
console.log(` ${step++}. Model server is already running at ${pc.cyan(profile.baseUrl)}${reason}`);
|
|
172
|
-
console.log(` ${pc.dim(`If you still need to open ${runnerLabel}, run: ${command}`)}`);
|
|
173
|
-
} else {
|
|
174
|
-
console.log(` ${step++}. If the model server is not already running, run: ${pc.cyan(command)}`);
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
console.log(` ${step++}. Open ${runnerLabel} for ${pc.bold(modelId)}`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
console.log(` ${step++}. ${pc.cyan(`cd ${repoPath}`)}`);
|
|
181
|
-
console.log(` ${step++}. ${pc.cyan("npm run dev")}`);
|
|
182
|
-
console.log(` ${step}. In the gallery, find this run, copy the prompt, and paste it into ${runnerLabel}`);
|
|
159
|
+
console.log(` 1. Open the gallery. If it is not running: ${pc.cyan(`cd ${repoPath} && npm run dev`)}`);
|
|
160
|
+
console.log(` 2. ${pc.cyan(`cd ${runDirectory}`)}`);
|
|
161
|
+
console.log(` 3. ${pc.cyan(runnerCommand)}, then copy this run's prompt from the gallery and paste it into ${runnerLabel}`);
|
|
183
162
|
}
|
|
184
163
|
|
|
185
164
|
async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSource, backendLabel, profile }) {
|
|
@@ -188,7 +167,6 @@ async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSo
|
|
|
188
167
|
const runId = createRunId(now);
|
|
189
168
|
const modelSlug = slugModelId(modelId);
|
|
190
169
|
const runnerLabel = intendedRunnerForProfile(profile);
|
|
191
|
-
const serverStatus = await profileServerStatus(profile);
|
|
192
170
|
const runsDir = join(repoPath, "runs");
|
|
193
171
|
const benchmarkDirectory = join(runsDir, benchmark.id);
|
|
194
172
|
const modelDirectory = join(benchmarkDirectory, modelSlug);
|
|
@@ -237,7 +215,7 @@ async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSo
|
|
|
237
215
|
["Source", backendLabel || modelSource],
|
|
238
216
|
])));
|
|
239
217
|
|
|
240
|
-
printBenchmarkNextSteps({ repoPath, profile, modelId, runnerLabel
|
|
218
|
+
printBenchmarkNextSteps({ repoPath, runDirectory, profile, modelId, runnerLabel });
|
|
241
219
|
|
|
242
220
|
return runDirectory;
|
|
243
221
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -299,6 +299,7 @@ function optionSourceTag(sourceId, label) {
|
|
|
299
299
|
ollama: pc.green,
|
|
300
300
|
omlx: pc.magenta,
|
|
301
301
|
gguf: pc.cyan,
|
|
302
|
+
hf: pc.cyan,
|
|
302
303
|
};
|
|
303
304
|
return optionTag(label, colors[sourceId] ?? pc.dim, OPTION_SOURCE_WIDTH);
|
|
304
305
|
}
|
|
@@ -331,13 +332,13 @@ function modelSelectOption(item, { runningProfilesNow, drafters }) {
|
|
|
331
332
|
if (item.type === "new") {
|
|
332
333
|
const { model, drafter } = item;
|
|
333
334
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
334
|
-
const mtpAvailable = caps.mtp || Boolean(drafter);
|
|
335
|
+
const mtpAvailable = caps.mtp || Boolean(drafter) || Boolean(model.hfRepo && /gemma-?4/i.test(`${model.hfRepo} ${model.label}`));
|
|
335
336
|
let mtpLabel;
|
|
336
337
|
if (mtpAvailable) mtpLabel = pc.green("MTP available");
|
|
337
338
|
else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP needs drafter");
|
|
338
339
|
const label = optionLabel({
|
|
339
340
|
status: optionStatusTag("setup"),
|
|
340
|
-
source: optionSourceTag("gguf", "GGUF file"),
|
|
341
|
+
source: optionSourceTag(model.hfRepo ? "hf" : "gguf", model.hfRepo ? "Hugging Face" : "GGUF file"),
|
|
341
342
|
name: model.label,
|
|
342
343
|
details: [pc.dim(formatBytes(model.sizeBytes)), mtpLabel],
|
|
343
344
|
});
|
|
@@ -434,7 +435,7 @@ function printModelCards(items, { runningProfilesNow, drafters }) {
|
|
|
434
435
|
for (const item of newModels) {
|
|
435
436
|
const { model, drafter } = item;
|
|
436
437
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
437
|
-
const mtpAvailable = caps.mtp || Boolean(drafter);
|
|
438
|
+
const mtpAvailable = caps.mtp || Boolean(drafter) || Boolean(model.hfRepo && /gemma-?4/i.test(`${model.hfRepo} ${model.label}`));
|
|
438
439
|
const mtpLabel = mtpAvailable
|
|
439
440
|
? pc.green("MTP ✓")
|
|
440
441
|
: (caps.architecture === "gemma4")
|
|
@@ -542,6 +543,7 @@ async function printProfileDetails(profile) {
|
|
|
542
543
|
];
|
|
543
544
|
if (!isManaged) {
|
|
544
545
|
detailRows.push(
|
|
546
|
+
...(profile.hfRepo ? [["Hugging Face", `${profile.hfRepo}${profile.hfVariant ? `:${profile.hfVariant}` : ""}`]] : []),
|
|
545
547
|
["Local file", fileMissing ? pc.red(`${profile.modelPath} (not found)`) : profile.modelPath ?? "unknown"],
|
|
546
548
|
["Vision file", profile.mmprojPath ? (existsSync(profile.mmprojPath) ? profile.mmprojPath : pc.red(`${profile.mmprojPath} (not found)`)) : "none"],
|
|
547
549
|
["Model size", profile.modelPath && existsSync(profile.modelPath) ? formatBytes(statSync(profile.modelPath).size) : "unknown"],
|
|
@@ -564,7 +566,7 @@ async function printProfileDetails(profile) {
|
|
|
564
566
|
|
|
565
567
|
function printGgufModelDetails(model, drafter) {
|
|
566
568
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
567
|
-
const mtpAvailable = caps.mtp || Boolean(drafter);
|
|
569
|
+
const mtpAvailable = caps.mtp || Boolean(drafter) || Boolean(model.hfRepo && /gemma-?4/i.test(`${model.hfRepo} ${model.label}`));
|
|
568
570
|
const mtpLabel = mtpAvailable
|
|
569
571
|
? pc.green("MTP ✓")
|
|
570
572
|
: (caps.architecture === "gemma4")
|
|
@@ -582,6 +584,7 @@ function printGgufModelDetails(model, drafter) {
|
|
|
582
584
|
];
|
|
583
585
|
console.log("\n" + renderSection("Downloaded model", renderRows(overviewRows)));
|
|
584
586
|
const detailRows = [
|
|
587
|
+
...(model.hfRepo ? [["Hugging Face", `${model.hfRepo}${model.hfVariant ? `:${model.hfVariant}` : ""}`]] : []),
|
|
585
588
|
["Local file", model.path],
|
|
586
589
|
["Vision file", model.mmprojPath ?? "none"],
|
|
587
590
|
["Detected", capabilitySummary(caps)],
|
package/src/profile-setup.mjs
CHANGED
|
@@ -34,14 +34,16 @@ 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
|
|
37
|
+
const hfSource = profile.hfRepo ? { hfRepo: profile.hfRepo, hfVariant: profile.hfVariant } : {};
|
|
38
|
+
const freshCaps = { ...detectCapabilities(profile.modelPath, profile.mmprojPath), ...hfSource };
|
|
38
39
|
let drafterPath = profile.drafterPath ?? null;
|
|
39
|
-
if (!drafterPath) {
|
|
40
|
+
if (!drafterPath && !profile.hfRepo) {
|
|
40
41
|
const { drafters } = await scanGgufModels();
|
|
41
42
|
const drafter = matchDrafter(profile.modelPath, drafters);
|
|
42
43
|
if (drafter) drafterPath = drafter.path;
|
|
43
44
|
}
|
|
44
|
-
const
|
|
45
|
+
const hfGemma4Mtp = Boolean(profile.hfRepo && /gemma-?4/i.test(`${profile.hfRepo} ${profile.label}`));
|
|
46
|
+
const hasMtp = freshCaps.mtp || Boolean(drafterPath) || hfGemma4Mtp;
|
|
45
47
|
const caps = { ...freshCaps, mtp: hasMtp };
|
|
46
48
|
// If MTP is newly available, switch backend and add drafter path
|
|
47
49
|
if (hasMtp && configured.backend !== "llama-cpp-mtp") {
|
|
@@ -71,7 +73,7 @@ export async function configureLocalProfile(prompt, profile) {
|
|
|
71
73
|
console.log(renderSection("MTP detected", renderRows([
|
|
72
74
|
["Backend", "llama.cpp MTP"],
|
|
73
75
|
["Port", String(LLAMA_CPP_MTP_PORT)],
|
|
74
|
-
["Flags", `--spec-type draft-mtp --spec-draft-n-max 4${configured.drafterPath ? " --spec-draft-model <drafter>" : ""}`],
|
|
76
|
+
["Flags", `--spec-type draft-mtp --spec-draft-n-max 4${configured.drafterPath && !configured.hfRepo ? " --spec-draft-model <drafter>" : ""}`],
|
|
75
77
|
])));
|
|
76
78
|
if (drafterInfo) console.log(pc.dim(drafterInfo));
|
|
77
79
|
const useMtp = await prompt.yesNo("Use MTP speculative decoding?", true);
|
|
@@ -142,7 +144,7 @@ function applyMtpDefaults(profile) {
|
|
|
142
144
|
const edits = {
|
|
143
145
|
values: { "--spec-type": "draft-mtp", "--spec-draft-n-max": 4 },
|
|
144
146
|
};
|
|
145
|
-
if (profile.drafterPath) {
|
|
147
|
+
if (profile.drafterPath && !profile.hfRepo) {
|
|
146
148
|
edits.values["--spec-draft-model"] = profile.drafterPath;
|
|
147
149
|
}
|
|
148
150
|
return applyProfileFlags({ ...profile, backend: "llama-cpp-mtp", providerId: "llama-cpp-mtp" }, flags, edits);
|
package/src/profiles.mjs
CHANGED
|
@@ -134,11 +134,15 @@ 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
|
-
|
|
137
|
+
// If a drafter is provided, this model supports MTP regardless of filename.
|
|
138
|
+
// For Hugging Face Gemma 4 GGUF repos, prefer llama.cpp's -hf companion-file
|
|
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;
|
|
139
142
|
const backend = backendId ?? (hasMtp ? "llama-cpp-mtp" : "llama-cpp");
|
|
143
|
+
const hfSource = model.hfRepo ? { hfRepo: model.hfRepo, hfVariant: model.hfVariant } : {};
|
|
140
144
|
const { flags, argv } = computeFlags(
|
|
141
|
-
{ ...caps, mtp: hasMtp },
|
|
145
|
+
{ ...caps, mtp: hasMtp, ...hfSource },
|
|
142
146
|
model.path,
|
|
143
147
|
model.mmprojPath,
|
|
144
148
|
drafterPath ?? null,
|
|
@@ -150,10 +154,12 @@ export async function createProfileFromModel(model, backendId, drafterPath) {
|
|
|
150
154
|
backend,
|
|
151
155
|
providerId: backend,
|
|
152
156
|
modelAlias: model.aliasSuggestion,
|
|
157
|
+
source: model.hfRepo ? "huggingface" : model.source,
|
|
153
158
|
modelPath: model.path,
|
|
154
|
-
mmprojPath: model.mmprojPath,
|
|
159
|
+
mmprojPath: model.hfRepo ? null : model.mmprojPath,
|
|
160
|
+
...hfSource,
|
|
155
161
|
drafterPath: drafterPath ?? null,
|
|
156
|
-
capabilities: summarizeCapabilities({ ...caps, mtp: hasMtp }),
|
|
162
|
+
capabilities: summarizeCapabilities({ ...caps, mtp: hasMtp, ...hfSource }),
|
|
157
163
|
preset: null, // no presets — auto-detected
|
|
158
164
|
flags,
|
|
159
165
|
commandArgv: argv,
|
|
@@ -171,6 +177,8 @@ function summarizeCapabilities(caps) {
|
|
|
171
177
|
quant: caps.quant,
|
|
172
178
|
metaCtx: caps.metaCtx,
|
|
173
179
|
mmprojProjectorType: caps.mmprojProjectorType,
|
|
180
|
+
hfRepo: caps.hfRepo,
|
|
181
|
+
hfVariant: caps.hfVariant,
|
|
174
182
|
ctxSize: caps.ctxSize,
|
|
175
183
|
};
|
|
176
184
|
}
|
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 } from "node:path";
|
|
3
|
+
import { basename, dirname, join, sep } from "node:path";
|
|
4
4
|
import { getModelScanDirs } from "./config.mjs";
|
|
5
5
|
import { readGgufMetadata } from "./gguf.mjs";
|
|
6
6
|
|
|
@@ -67,6 +67,7 @@ async function scanOneDir(root) {
|
|
|
67
67
|
source: "local-gguf",
|
|
68
68
|
});
|
|
69
69
|
} else {
|
|
70
|
+
const hf = inferHfRef(path, name);
|
|
70
71
|
models.push({
|
|
71
72
|
path,
|
|
72
73
|
mmprojPath,
|
|
@@ -75,7 +76,8 @@ async function scanOneDir(root) {
|
|
|
75
76
|
quant: quantFromName(name),
|
|
76
77
|
sizeBytes,
|
|
77
78
|
backend: "llama-cpp",
|
|
78
|
-
source: "local-gguf",
|
|
79
|
+
source: hf ? "huggingface" : "local-gguf",
|
|
80
|
+
...(hf ? { hfRepo: hf.repo, hfVariant: hf.variant } : {}),
|
|
79
81
|
});
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -158,6 +160,24 @@ function quantFromName(name) {
|
|
|
158
160
|
return name.match(/(Q\d_K_[A-Z]+|Q\d_[01]|UD-[A-Z0-9_]+)/)?.[1];
|
|
159
161
|
}
|
|
160
162
|
|
|
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
|
function safeReadGgufMetadata(path) {
|
|
162
182
|
try {
|
|
163
183
|
return readGgufMetadata(path);
|