offgrid-ai 0.3.20 → 0.3.22

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.3.20",
3
+ "version": "0.3.22",
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",
package/src/backends.mjs CHANGED
@@ -72,16 +72,18 @@ async function scanOllamaModels() {
72
72
  if (!response.ok) return [];
73
73
  const body = await response.json();
74
74
  if (!Array.isArray(body?.models)) return [];
75
- return body.models.map((model) => ({
76
- id: model.name,
77
- label: ollamaLabel(model.name),
78
- aliasSuggestion: model.name,
79
- sizeBytes: model.size ?? 0,
80
- quant: model.details?.quantization_level,
81
- family: model.details?.family,
82
- backend: "ollama",
83
- source: "ollama",
84
- })).sort((a, b) => a.label.localeCompare(b.label));
75
+ return body.models
76
+ .filter((model) => isLocalOllamaModel(model))
77
+ .map((model) => ({
78
+ id: model.name,
79
+ label: ollamaLabel(model.name),
80
+ aliasSuggestion: model.name,
81
+ sizeBytes: model.size ?? 0,
82
+ quant: model.details?.quantization_level,
83
+ family: model.details?.family,
84
+ backend: "ollama",
85
+ source: "ollama",
86
+ })).sort((a, b) => a.label.localeCompare(b.label));
85
87
  } catch {
86
88
  return [];
87
89
  }
@@ -112,6 +114,13 @@ async function scanOmlxModels() {
112
114
 
113
115
  // ── Labels ──────────────────────────────────────────────────────────────
114
116
 
117
+ function isLocalOllamaModel(model) {
118
+ const name = String(model?.name ?? "");
119
+ if (/:cloud(?:$|\b)/i.test(name)) return false;
120
+ if (!Number.isFinite(model?.size) || model.size <= 0) return false;
121
+ return true;
122
+ }
123
+
115
124
  function ollamaLabel(name) {
116
125
  return name.replace(/[-_]/g, " ").replace(/^gemma\b/i, "Gemma").replace(/^qwen/i, "Qwen");
117
126
  }
package/src/cli.mjs CHANGED
@@ -280,7 +280,7 @@ async function printModelCatalog({ profiles, newModels, managedItems }, items =
280
280
  const backendItems = managedItems.filter((item) => item.backendId === backendId);
281
281
  if (backendItems.length === 0) continue;
282
282
  const be = BACKENDS[backendId];
283
- console.log("\n" + pc.bold(`Available from ${be.label}`));
283
+ console.log("\n" + pc.bold(`Local models via ${be.label}`));
284
284
  for (const { model } of backendItems.slice(0, 10)) {
285
285
  const num = itemNumber((item) => item.type === "managed" && item.backendId === backendId && item.model.id === model.id);
286
286
  console.log(managedModelCard(num, model, be));
@@ -307,13 +307,13 @@ function downloadedModelCard(num, model, caps) {
307
307
  ["Good for", humanCapabilitySummary(caps)],
308
308
  ["Size", formatBytes(model.sizeBytes)],
309
309
  ["When selected", "offgrid-ai will recommend safe local settings"],
310
- ]), { formatBorder: caps.mtp ? pc.blue : pc.yellow });
310
+ ]), { formatBorder: pc.yellow });
311
311
  }
312
312
 
313
313
  function managedModelCard(num, model, backend) {
314
314
  return renderCard(`${num}. ${model.label}`, renderRows([
315
- ["Status", statusText("info", "Available from another app")],
316
- ["App", backend.label],
315
+ ["Status", statusText("info", `Local model via ${backend.label}`)],
316
+ ["Runs with", backend.label],
317
317
  ["Model ID", pc.cyan(model.id)],
318
318
  ...(model.quant ? [["Size/type", model.quant]] : []),
319
319
  ]), { formatBorder: pc.magenta });
@@ -438,7 +438,7 @@ function printGgufModelDetails(model) {
438
438
  function printManagedModelDetails(model, backend) {
439
439
  console.log("\n" + renderSection(`${backend.label} model`, renderRows([
440
440
  ["Name", pc.bold(model.label)],
441
- ["Status", pc.green("Available from another app")],
441
+ ["Status", pc.green(`Local model via ${backend.label}`)],
442
442
  ["Model ID", pc.cyan(model.id)],
443
443
  ["Quant", model.quant ?? "unknown"],
444
444
  ["Family", model.family ?? "unknown"],
@@ -1,11 +1,11 @@
1
1
  import { estimateMemory } from "./estimate.mjs";
2
- import { pc, formatBytes, renderRows, renderSection, humanCapabilitySummary } from "./ui.mjs";
2
+ import { pc, formatBytes, renderRows, renderSection } from "./ui.mjs";
3
3
 
4
4
  const CACHE_CHOICES = [
5
- { value: "bf16", label: "Balanced", hint: "recommended: stable, good quality" },
6
- { value: "f16", label: "Compatible", hint: "stable fallback, similar memory use" },
7
- { value: "q8_0", label: "Lower memory", hint: "usually safe, uses less memory" },
8
- { value: "q4_0", label: "Smallest memory", hint: "maximum savings, quality/speed tradeoff" },
5
+ { value: "bf16", label: "bf16", hint: "default: stable, good quality" },
6
+ { value: "f16", label: "f16", hint: "stable fallback, similar memory to bf16" },
7
+ { value: "q8_0", label: "q8_0", hint: "lower memory, usually safe" },
8
+ { value: "q4_0", label: "q4_0", hint: "lowest memory, quality/speed tradeoff" },
9
9
  ];
10
10
 
11
11
  const GENERAL_DEFAULTS = {
@@ -26,49 +26,64 @@ export async function configureLocalProfile(prompt, profile) {
26
26
  const caps = profile.capabilities ?? {};
27
27
 
28
28
  console.log("");
29
- console.log(renderSection("Let's set up this model", renderRows([
29
+ console.log(renderSection("Model setup", renderRows([
30
30
  ["Model", pc.bold(profile.label)],
31
- ["Good for", humanCapabilitySummary(caps)],
32
- ["Conversation memory", `${profile.flags.ctxSize.toLocaleString()} tokens`],
33
- ["Memory mode", `${profile.flags.cacheTypeK}/${profile.flags.cacheTypeV}`],
34
- ["Response style", samplingSummary(profile.flags)],
31
+ ["Detected", detectionSummary(caps)],
32
+ ["Context", `${profile.flags.ctxSize.toLocaleString()} tokens`],
33
+ ["KV cache", `${profile.flags.cacheTypeK}/${profile.flags.cacheTypeV}`],
34
+ ["Sampling", samplingSummary(profile.flags)],
35
35
  ])));
36
- console.log(pc.dim("You can accept the recommended settings. Bigger conversation memory uses more RAM.\n"));
36
+ console.log(pc.dim("Larger context windows use more memory. KV cache precision controls memory used by attention history."));
37
+ console.log(pc.dim("Sampling defaults are shown for transparency; you can edit command.json later if needed.\n"));
37
38
 
38
39
  if (caps.mtp) {
39
- console.log(renderSection("Speed boost available", "This model supports fast draft mode. It can make responses feel faster while keeping everything local.\n\nAdvanced: uses llama.cpp MTP on port 8081."));
40
- const useMtp = await prompt.yesNo("Use fast draft mode for this model?", true);
40
+ console.log(renderSection("MTP detected", renderRows([
41
+ ["Backend", "llama.cpp MTP"],
42
+ ["Port", "8081"],
43
+ ["Flags", "--spec-type draft-mtp --spec-draft-n-max 2"],
44
+ ])));
45
+ const useMtp = await prompt.yesNo("Use MTP speculative decoding flags?", true);
41
46
  configured = useMtp ? applyMtpDefaults(configured) : removeMtpDefaults(configured);
42
47
  }
43
48
 
44
49
  if (caps.qat) {
45
50
  console.log("");
46
- console.log(renderSection("Optimized model", "This model was trained to work well after compression. No extra runtime settings are needed."));
51
+ console.log(renderSection("QAT detected", renderRows([
52
+ ["Meaning", "quantization-aware trained"],
53
+ ["Runtime flags", "none required"],
54
+ ])));
47
55
  }
48
56
 
49
57
  if (caps.thinking) {
50
58
  console.log("");
51
- console.log(renderSection("Reasoning mode", "This model can reason step by step. offgrid-ai can use safer defaults that reduce repetitive loops."));
52
- const useThinking = await prompt.yesNo("Use reasoning-friendly defaults?", true);
59
+ console.log(renderSection("Thinking model detected", renderRows([
60
+ ["Defaults", "thinking / loop-safe"],
61
+ ["Flags", "--top-k 64 --presence-penalty 0 --repeat-penalty 1.1"],
62
+ ["Template", "--chat-template-kwargs { enable_thinking: true }"],
63
+ ])));
64
+ const useThinking = await prompt.yesNo("Use these thinking/loop-safe defaults?", true);
53
65
  configured = useThinking ? applyThinkingDefaults(configured) : removeThinkingDefaults(configured);
54
66
  }
55
67
 
56
- const ctxSize = await prompt.number("Conversation memory tokens", configured.flags.ctxSize, 1024, 1048576);
57
- const cacheTypeK = await prompt.choice("Memory mode, part 1", CACHE_CHOICES, configured.flags.cacheTypeK);
58
- const cacheTypeV = await prompt.choice("Memory mode, part 2", CACHE_CHOICES, configured.flags.cacheTypeV);
68
+ const ctxSize = await prompt.number("Context window tokens", configured.flags.ctxSize, 1024, 1048576);
69
+ const cacheTypeK = await prompt.choice("K cache precision", CACHE_CHOICES, configured.flags.cacheTypeK);
70
+ const cacheTypeV = await prompt.choice("V cache precision", CACHE_CHOICES, configured.flags.cacheTypeV);
59
71
  configured = applyRuntimeFlagOverrides(configured, { ctxSize, cacheTypeK, cacheTypeV });
60
72
 
61
73
  console.log("");
62
- console.log(renderSection("Final setup", renderRows([
63
- ["Runs with", configured.backend],
64
- ["Local address", configured.baseUrl],
65
- ["Creativity", configured.flags.temperature],
66
- ["Focus", configured.flags.topP],
67
- ["Reasoning breadth", configured.flags.topK],
74
+ console.log(renderSection("Defaults", renderRows([
75
+ ["Backend", configured.backend],
76
+ ["Endpoint", configured.baseUrl],
77
+ ["Temperature", configured.flags.temperature],
78
+ ["Top-p", configured.flags.topP],
79
+ ["Top-k", configured.flags.topK],
80
+ ["Min-p", configured.flags.minP],
81
+ ["Presence penalty", configured.flags.presencePenalty],
82
+ ["Repeat penalty", configured.flags.repeatPenalty],
68
83
  ])));
69
84
 
70
85
  console.log("\n" + renderMemoryEstimate(configured));
71
- if (!(await prompt.yesNo("Save this model setup?", true))) return null;
86
+ if (!(await prompt.yesNo("Save profile with these settings?", true))) return null;
72
87
  return configured;
73
88
  }
74
89
 
@@ -155,8 +170,8 @@ function renderMemoryEstimate(profile) {
155
170
  const est = estimateMemory(profile.modelPath, profile.mmprojPath, null, profile.flags);
156
171
  return renderSection("Memory estimate", renderRows([
157
172
  ["Estimated total", pc.bold(`~${formatBytes(est.totalBytes)}`)],
158
- ["Model file", formatBytes(est.modelBytes)],
159
- ["Conversation memory", est.kvBytes ? `~${formatBytes(est.kvBytes)} (${profile.flags.ctxSize.toLocaleString()} tokens, ${profile.flags.cacheTypeK}/${profile.flags.cacheTypeV})` : "unknown"],
173
+ ["Model", formatBytes(est.modelBytes)],
174
+ ["KV cache", est.kvBytes ? `~${formatBytes(est.kvBytes)} (${profile.flags.ctxSize.toLocaleString()} ctx, ${profile.flags.cacheTypeK}/${profile.flags.cacheTypeV})` : "unknown"],
160
175
  ...(est.note ? [["Note", pc.yellow(est.note)]] : []),
161
176
  ]));
162
177
  } catch {
@@ -164,6 +179,18 @@ function renderMemoryEstimate(profile) {
164
179
  }
165
180
  }
166
181
 
182
+ function detectionSummary(caps) {
183
+ const parts = [];
184
+ if (caps.architecture) parts.push(caps.architecture);
185
+ if (caps.quant) parts.push(caps.quant);
186
+ if (caps.mtp) parts.push("MTP");
187
+ if (caps.qat) parts.push("QAT");
188
+
189
+ if (caps.thinking) parts.push("thinking");
190
+ if (caps.vision) parts.push("vision");
191
+ return parts.length > 0 ? parts.join(" · ") : "standard GGUF";
192
+ }
193
+
167
194
  function samplingSummary(flags) {
168
195
  return `temp ${flags.temperature}, top-p ${flags.topP}, top-k ${flags.topK}`;
169
196
  }
package/src/ui.mjs CHANGED
@@ -83,8 +83,8 @@ export function humanCapabilitySummary(caps = {}) {
83
83
  const parts = [];
84
84
  if (caps.thinking) parts.push(pc.magenta("Reasoning"));
85
85
  if (caps.vision) parts.push(pc.cyan("Vision"));
86
- if (caps.mtp) parts.push(pc.blue("Fast draft mode"));
87
- if (caps.qat) parts.push(pc.green("Optimized quantization"));
86
+ if (caps.mtp) parts.push(pc.blue("MTP"));
87
+ if (caps.qat) parts.push(pc.green("QAT"));
88
88
  return parts.length > 0 ? parts.join(" · ") : "General chat";
89
89
  }
90
90