offgrid-ai 0.11.0 → 0.11.3

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.11.0",
3
+ "version": "0.11.3",
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
@@ -1,7 +1,7 @@
1
1
  import { findLlamaServer } from "./config.mjs";
2
2
  import { scanGgufModels } from "./scan.mjs";
3
3
  import { parseModelName } from "./model-name.mjs";
4
- import { scanMlxModels, scanOmlxModelSizes, lookupOmlxModelSize } from "./mlx-discovery.mjs";
4
+ import { scanMlxModels, scanOmlxModelSizes, lookupOmlxModelInfo } from "./mlx-discovery.mjs";
5
5
  import { DEFAULT_PORT as MLX_VLM_PORT } from "./mlx-flags.mjs";
6
6
 
7
7
  // ── Backend definitions ────────────────────────────────────────────────────
@@ -96,19 +96,23 @@ async function scanOmlxModels() {
96
96
  const body = await response.json();
97
97
  if (!Array.isArray(body?.data)) return [];
98
98
 
99
- // The oMLX API doesn't return model sizes — look them up from disk.
100
- const sizeMap = await scanOmlxModelSizes();
99
+ // The oMLX API doesn't return model sizes or publishers — look them up from disk.
100
+ const infoMap = await scanOmlxModelSizes();
101
101
 
102
102
  return body.data
103
103
  .filter((model) => isChatOmlxModel(model))
104
104
  .map((model) => {
105
- const sizeFromDisk = lookupOmlxModelSize(model.id, sizeMap);
106
- const parsed = parseModelName(model.id, "omlx");
105
+ const info = lookupOmlxModelInfo(model.id, infoMap);
106
+ // If the API ID doesn't already include a publisher (no / or --),
107
+ // prepend the publisher found on disk.
108
+ const hasPublisher = model.id.includes("/") || model.id.includes("--");
109
+ const fullName = (!hasPublisher && info?.publisher) ? `${info.publisher}/${model.id}` : model.id;
110
+ const parsed = parseModelName(fullName, "omlx");
107
111
  return {
108
112
  id: model.id,
109
113
  label: parsed.display,
110
114
  aliasSuggestion: model.id,
111
- sizeBytes: sizeFromDisk ?? (model.size ?? 0),
115
+ sizeBytes: info?.sizeBytes ?? (model.size ?? 0),
112
116
  contextLength: model.max_model_len ?? null,
113
117
  quant: parsed.quant,
114
118
  family: null,
@@ -308,14 +308,14 @@ export function defaultMlxContextLength(trainedCtx, ramGb) {
308
308
 
309
309
  /**
310
310
  * Scan the oMLX models directory (~/.omlx/models/) for MLX model directories
311
- * and return a Map of basename → sizeBytes. The oMLX API doesn't return model
312
- * sizes, so we compute them from the safetensors files on disk.
311
+ * and return a Map of basename → { sizeBytes, publisher }. The oMLX API
312
+ * doesn't return model sizes or publishers, so we compute them from disk.
313
313
  */
314
314
  export async function scanOmlxModelSizes() {
315
315
  if (!existsSync(OMLX_MODELS_DIR)) return new Map();
316
- const sizeByBasename = new Map();
316
+ const infoByBasename = new Map();
317
317
 
318
- async function walk(dir) {
318
+ async function walk(dir, publisher) {
319
319
  let entries;
320
320
  try {
321
321
  entries = await readdir(dir, { withFileTypes: true });
@@ -327,26 +327,27 @@ export async function scanOmlxModelSizes() {
327
327
  const fullPath = join(dir, entry.name);
328
328
  if (await isMlxModelDir(fullPath)) {
329
329
  const sizeBytes = await getMlxDirSizeBytes(fullPath);
330
- if (sizeBytes > 0) sizeByBasename.set(entry.name, sizeBytes);
330
+ if (sizeBytes > 0) infoByBasename.set(entry.name, { sizeBytes, publisher });
331
331
  } else {
332
- await walk(fullPath);
332
+ // First-level directories under ~/.omlx/models/ are publishers
333
+ await walk(fullPath, publisher ?? entry.name);
333
334
  }
334
335
  }
335
336
  }
336
337
 
337
- await walk(OMLX_MODELS_DIR);
338
- return sizeByBasename;
338
+ await walk(OMLX_MODELS_DIR, null);
339
+ return infoByBasename;
339
340
  }
340
341
 
341
342
  /**
342
- * Look up a model's size by its oMLX API id. Tries exact match, then the
343
+ * Look up a model's info by its oMLX API id. Tries exact match, then the
343
344
  * segment after `--` (oMLX org--name format), then after `/` (HF format).
344
345
  */
345
- export function lookupOmlxModelSize(modelId, sizeMap) {
346
- if (sizeMap.has(modelId)) return sizeMap.get(modelId);
346
+ export function lookupOmlxModelInfo(modelId, infoMap) {
347
+ if (infoMap.has(modelId)) return infoMap.get(modelId);
347
348
  const dashIdx = modelId.indexOf("--");
348
- if (dashIdx >= 0 && sizeMap.has(modelId.slice(dashIdx + 2))) return sizeMap.get(modelId.slice(dashIdx + 2));
349
+ if (dashIdx >= 0 && infoMap.has(modelId.slice(dashIdx + 2))) return infoMap.get(modelId.slice(dashIdx + 2));
349
350
  const slashIdx = modelId.indexOf("/");
350
- if (slashIdx >= 0 && sizeMap.has(modelId.slice(slashIdx + 1))) return sizeMap.get(modelId.slice(slashIdx + 1));
351
+ if (slashIdx >= 0 && infoMap.has(modelId.slice(slashIdx + 1))) return infoMap.get(modelId.slice(slashIdx + 1));
351
352
  return null;
352
353
  }
@@ -195,6 +195,9 @@ function titleCaseModel(name) {
195
195
  // Title-case standalone aXb patterns (A3B, A12B — active parameters)
196
196
  result = result.replace(/\ba(\d+)\s*b\b/gi, (_, num) => `A${num}B`);
197
197
 
198
+ // Title-case eXb patterns (E2B, E12B — effective parameters)
199
+ result = result.replace(/\be(\d+)\s*b\b/gi, (_, num) => `E${num}B`);
200
+
198
201
  // Clean up extra spaces
199
202
  result = result.replace(/\s{2,}/g, " ").trim();
200
203
 
package/src/runtime.mjs CHANGED
@@ -26,7 +26,7 @@ export async function offerManagedLlamaRuntimeUpdate(prompt, { fetchImpl = globa
26
26
  ["Source", "official GitHub release binary"],
27
27
  ]), { formatBorder: pc.cyan }));
28
28
 
29
- const shouldInstall = await prompt.yesNo(installed ? "Update llama.cpp runtime?" : "Install llama.cpp runtime?", true);
29
+ const shouldInstall = await prompt.yesNo(installed ? "Update llama.cpp runtime?" : "Install llama.cpp runtime?", false);
30
30
  if (!shouldInstall) return false;
31
31
 
32
32
  await installLlamaRelease(latest, { fetchImpl });