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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "Privacy-first CLI for running local LLMs — discover, configure, run, benchmark",
5
5
  "author": "Eeshan Srivastava (https://eeshans.com)",
6
6
  "type": "module",
@@ -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. 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);
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.map((model) => ({
121
- id: model.id,
122
- label: omlxLabel(model.id),
123
- aliasSuggestion: model.id,
124
- sizeBytes: 0,
125
- quant: null,
126
- family: null,
127
- backend: "omlx",
128
- source: "omlx",
129
- })).sort((a, b) => a.label.localeCompare(b.label));
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: ["SET UP", pc.green],
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 optionHint(parts) {
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 caps = profile.capabilities ?? {};
324
- let mtpLabel;
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, drafter } = item;
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(model.hfRepo ? "hf" : "gguf", model.hfRepo ? "Hugging Face" : "GGUF file"),
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, hint: optionHint([basename(model.path), humanCapabilitySummary(caps), model.quant]) };
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, hint: optionHint([model.id, "available through local backend"]) };
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) || Boolean(model.hfRepo && /gemma-?4/i.test(`${model.hfRepo} ${model.label}`));
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) || Boolean(model.hfRepo && /gemma-?4/i.test(`${model.hfRepo} ${model.label}`));
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)],
@@ -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 hfSource = profile.hfRepo ? { hfRepo: profile.hfRepo, hfVariant: profile.hfVariant } : {};
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 && !profile.hfRepo) {
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 hfGemma4Mtp = Boolean(profile.hfRepo && /gemma-?4/i.test(`${profile.hfRepo} ${profile.label}`));
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 && !configured.hfRepo ? " --spec-draft-model <drafter>" : ""}`],
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 && !profile.hfRepo) {
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
- // 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;
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, ...hfSource },
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.hfRepo ? "huggingface" : model.source,
153
+ source: model.source,
158
154
  modelPath: model.path,
159
- mmprojPath: model.hfRepo ? null : model.mmprojPath,
160
- ...hfSource,
155
+ mmprojPath: model.mmprojPath,
161
156
  drafterPath: drafterPath ?? null,
162
- capabilities: summarizeCapabilities({ ...caps, mtp: hasMtp, ...hfSource }),
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, sep } from "node:path";
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: hf ? "huggingface" : "local-gguf",
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 {