offgrid-ai 0.10.1 → 0.10.2
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/backends.mjs +19 -12
- package/src/mlx-discovery.mjs +47 -0
- package/src/model-presenters.mjs +5 -2
package/package.json
CHANGED
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 } from "./mlx-discovery.mjs";
|
|
4
|
+
import { scanMlxModels, scanOmlxModelSizes, lookupOmlxModelSize } from "./mlx-discovery.mjs";
|
|
5
5
|
import { DEFAULT_PORT as MLX_VLM_PORT } from "./mlx-flags.mjs";
|
|
6
6
|
|
|
7
7
|
// ── Backend definitions ────────────────────────────────────────────────────
|
|
@@ -95,19 +95,26 @@ async function scanOmlxModels() {
|
|
|
95
95
|
}
|
|
96
96
|
const body = await response.json();
|
|
97
97
|
if (!Array.isArray(body?.data)) return [];
|
|
98
|
+
|
|
99
|
+
// The oMLX API doesn't return model sizes — look them up from disk.
|
|
100
|
+
const sizeMap = await scanOmlxModelSizes();
|
|
101
|
+
|
|
98
102
|
return body.data
|
|
99
103
|
.filter((model) => isChatOmlxModel(model))
|
|
100
|
-
.map((model) =>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
.map((model) => {
|
|
105
|
+
const sizeFromDisk = lookupOmlxModelSize(model.id, sizeMap);
|
|
106
|
+
return {
|
|
107
|
+
id: model.id,
|
|
108
|
+
label: parseModelName(model.id, "omlx").display,
|
|
109
|
+
aliasSuggestion: model.id,
|
|
110
|
+
sizeBytes: sizeFromDisk ?? (model.size ?? 0),
|
|
111
|
+
contextLength: model.max_model_len ?? null,
|
|
112
|
+
quant: null,
|
|
113
|
+
family: null,
|
|
114
|
+
backend: "omlx",
|
|
115
|
+
source: "omlx",
|
|
116
|
+
};
|
|
117
|
+
}).sort((a, b) => a.label.localeCompare(b.label));
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
// ── Labels ──────────────────────────────────────────────────────────────
|
package/src/mlx-discovery.mjs
CHANGED
|
@@ -291,4 +291,51 @@ export function defaultMlxContextLength(trainedCtx, ramGb) {
|
|
|
291
291
|
if (ramGb < 16) return Math.min(trainedCtx, 8192);
|
|
292
292
|
if (ramGb < 32) return Math.min(trainedCtx, 16384);
|
|
293
293
|
return trainedCtx;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── oMLX model size lookup (from disk) ────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Scan the oMLX models directory (~/.omlx/models/) for MLX model directories
|
|
300
|
+
* and return a Map of basename → sizeBytes. The oMLX API doesn't return model
|
|
301
|
+
* sizes, so we compute them from the safetensors files on disk.
|
|
302
|
+
*/
|
|
303
|
+
export async function scanOmlxModelSizes() {
|
|
304
|
+
if (!existsSync(OMLX_MODELS_DIR)) return new Map();
|
|
305
|
+
const sizeByBasename = new Map();
|
|
306
|
+
|
|
307
|
+
async function walk(dir) {
|
|
308
|
+
let entries;
|
|
309
|
+
try {
|
|
310
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
311
|
+
} catch {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
if (!entry.isDirectory()) continue;
|
|
316
|
+
const fullPath = join(dir, entry.name);
|
|
317
|
+
if (await isMlxModelDir(fullPath)) {
|
|
318
|
+
const sizeBytes = await getMlxDirSizeBytes(fullPath);
|
|
319
|
+
if (sizeBytes > 0) sizeByBasename.set(entry.name, sizeBytes);
|
|
320
|
+
} else {
|
|
321
|
+
await walk(fullPath);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await walk(OMLX_MODELS_DIR);
|
|
327
|
+
return sizeByBasename;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Look up a model's size by its oMLX API id. Tries exact match, then the
|
|
332
|
+
* segment after `--` (oMLX org--name format), then after `/` (HF format).
|
|
333
|
+
*/
|
|
334
|
+
export function lookupOmlxModelSize(modelId, sizeMap) {
|
|
335
|
+
if (sizeMap.has(modelId)) return sizeMap.get(modelId);
|
|
336
|
+
const dashIdx = modelId.indexOf("--");
|
|
337
|
+
if (dashIdx >= 0 && sizeMap.has(modelId.slice(dashIdx + 2))) return sizeMap.get(modelId.slice(dashIdx + 2));
|
|
338
|
+
const slashIdx = modelId.indexOf("/");
|
|
339
|
+
if (slashIdx >= 0 && sizeMap.has(modelId.slice(slashIdx + 1))) return sizeMap.get(modelId.slice(slashIdx + 1));
|
|
340
|
+
return null;
|
|
294
341
|
}
|
package/src/model-presenters.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { DATA_DIR } from "./config.mjs";
|
|
|
11
11
|
import { findBenchmarkRepo } from "./benchmark.mjs";
|
|
12
12
|
|
|
13
13
|
const OPTION_SEPARATOR = pc.dim(" │ ");
|
|
14
|
-
const OPTION_STATUS_WIDTH =
|
|
14
|
+
const OPTION_STATUS_WIDTH = 12;
|
|
15
15
|
const OPTION_BACKEND_WIDTH = 14;
|
|
16
16
|
const OPTION_SOURCE_WIDTH = 14;
|
|
17
17
|
const OPTION_CTX_WIDTH = 5;
|
|
@@ -30,7 +30,7 @@ function optionStatusTag(kind) {
|
|
|
30
30
|
serverup: ["READY", pc.blue],
|
|
31
31
|
ready: ["READY", pc.blue],
|
|
32
32
|
missing: ["MISSING", pc.red],
|
|
33
|
-
setup: ["SETUP", pc.yellow],
|
|
33
|
+
setup: ["NEEDS SETUP", pc.yellow],
|
|
34
34
|
};
|
|
35
35
|
const [text, color] = statuses[kind] ?? [kind, pc.dim];
|
|
36
36
|
return optionPad(text, color, OPTION_STATUS_WIDTH);
|
|
@@ -101,6 +101,9 @@ function discoverySourceForItem(item) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function optionCtxLabel(item) {
|
|
104
|
+
// Context window is a configured value — only profiles (READY/RUNNING)
|
|
105
|
+
// have one. SETUP items (new/managed) show "—".
|
|
106
|
+
if (item.type !== "profile") return optionPad("—", null, OPTION_CTX_WIDTH);
|
|
104
107
|
if (item.contextLength) {
|
|
105
108
|
return optionPad(`${(item.contextLength / 1000).toFixed(0)}k`, null, OPTION_CTX_WIDTH);
|
|
106
109
|
}
|