offgrid-ai 0.10.0 → 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 -11
- package/src/commands/main.mjs +4 -2
- package/src/commands/models.mjs +1 -1
- package/src/mlx-discovery.mjs +54 -3
- package/src/model-catalog.mjs +66 -5
- package/src/model-presenters.mjs +15 -40
- package/src/scan.mjs +4 -0
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,18 +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
|
-
|
|
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));
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
// ── Labels ──────────────────────────────────────────────────────────────
|
package/src/commands/main.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { findLlamaServer, ensureDirs } from "../config.mjs";
|
|
2
2
|
import { backendFor } from "../backends.mjs";
|
|
3
3
|
import { scanGgufModels } from "../scan.mjs";
|
|
4
|
+
import { scanMlxModels } from "../mlx-discovery.mjs";
|
|
4
5
|
import { loadProfiles } from "../profiles.mjs";
|
|
5
6
|
import { hasPi } from "../harness-pi.mjs";
|
|
6
7
|
import { offerManagedLlamaRuntimeUpdate } from "../runtime.mjs";
|
|
@@ -26,9 +27,10 @@ export async function mainFlow() {
|
|
|
26
27
|
const llamaBinary = await findLlamaServer();
|
|
27
28
|
const { models: ggufModels, drafters } = await scanGgufModels();
|
|
28
29
|
const managedModels = await scanManagedModels();
|
|
30
|
+
const mlxModels = await scanMlxModels();
|
|
29
31
|
const profiles = await loadProfiles();
|
|
30
32
|
const hasAnyBackend = llamaBinary || managedModels.some((item) => item.status === "ok" && item.models.length > 0);
|
|
31
|
-
const hasAnyModels = ggufModels.length > 0 || managedModels.some((item) => item.status === "ok" && item.models.length > 0);
|
|
33
|
+
const hasAnyModels = ggufModels.length > 0 || mlxModels.length > 0 || managedModels.some((item) => item.status === "ok" && item.models.length > 0);
|
|
32
34
|
|
|
33
35
|
const piInstalled = await hasPi();
|
|
34
36
|
const needsLlama = ggufModels.length > 0 || profiles.some((profile) => backendFor(profile.backend).type === "local-server");
|
|
@@ -56,7 +58,7 @@ export async function mainFlow() {
|
|
|
56
58
|
if (!process.stdin.isTTY) return await statusCommand();
|
|
57
59
|
|
|
58
60
|
startInteractive("offgrid-ai");
|
|
59
|
-
return await modelCommandCenter({ profiles, ggufModels, managedModels, drafters });
|
|
61
|
+
return await modelCommandCenter({ profiles, ggufModels, managedModels, mlxModels, drafters });
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
async function printNoModelsHelp(llamaBinary) {
|
package/src/commands/models.mjs
CHANGED
|
@@ -74,7 +74,7 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
74
74
|
const bucket = grouped.get(group);
|
|
75
75
|
if (!bucket || bucket.length === 0) continue;
|
|
76
76
|
for (const item of bucket) {
|
|
77
|
-
const opt = modelSelectOption(item, { runningProfilesNow, modelMissingIds, nameWidth
|
|
77
|
+
const opt = modelSelectOption(item, { runningProfilesNow, modelMissingIds, nameWidth });
|
|
78
78
|
choices.push({ value: opt.value, label: opt.label, hint: opt.hint });
|
|
79
79
|
}
|
|
80
80
|
}
|
package/src/mlx-discovery.mjs
CHANGED
|
@@ -80,7 +80,8 @@ async function scanDirRecursiveForMlx(rootDir, sourceLabel, maxDepth = 3) {
|
|
|
80
80
|
const sizeBytes = await getMlxDirSizeBytes(dir);
|
|
81
81
|
if (sizeBytes < MIN_MODEL_SIZE_BYTES) return;
|
|
82
82
|
if (await isEmbeddingMlxModel(join(dir, "config.json"))) return;
|
|
83
|
-
|
|
83
|
+
const caps = await detectMlxCapabilities(dir);
|
|
84
|
+
models.push(makeMlxModel(dir, basename(dir), sizeBytes, sourceLabel, rootDir, caps.contextLength));
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -92,7 +93,8 @@ async function scanDirRecursiveForMlx(rootDir, sourceLabel, maxDepth = 3) {
|
|
|
92
93
|
const sizeBytes = await getMlxDirSizeBytes(fullPath);
|
|
93
94
|
if (sizeBytes < MIN_MODEL_SIZE_BYTES) continue;
|
|
94
95
|
if (await isEmbeddingMlxModel(join(fullPath, "config.json"))) continue;
|
|
95
|
-
|
|
96
|
+
const caps = await detectMlxCapabilities(fullPath);
|
|
97
|
+
models.push(makeMlxModel(fullPath, entry.name, sizeBytes, sourceLabel, rootDir, caps.contextLength));
|
|
96
98
|
} else {
|
|
97
99
|
await walk(fullPath, depth + 1);
|
|
98
100
|
}
|
|
@@ -155,6 +157,7 @@ async function scanHfHubForMlx(dir, sourceLabel) {
|
|
|
155
157
|
path: snapshotPath,
|
|
156
158
|
filePath: snapshotPath,
|
|
157
159
|
sizeBytes,
|
|
160
|
+
contextLength: (await detectMlxCapabilities(snapshotPath)).contextLength,
|
|
158
161
|
backend: "mlx-vlm",
|
|
159
162
|
format: "mlx",
|
|
160
163
|
source: sourceLabel,
|
|
@@ -185,13 +188,14 @@ async function isEmbeddingMlxModel(configPath) {
|
|
|
185
188
|
|
|
186
189
|
// ── MLX model entry builder ───────────────────────────────────────────────
|
|
187
190
|
|
|
188
|
-
function makeMlxModel(dir, label, sizeBytes, sourceLabel, rootDir) {
|
|
191
|
+
function makeMlxModel(dir, label, sizeBytes, sourceLabel, rootDir, contextLength = null) {
|
|
189
192
|
return {
|
|
190
193
|
id: `${sourceLabel}:${dir.replace(rootDir + "/", "")}`,
|
|
191
194
|
label,
|
|
192
195
|
path: dir,
|
|
193
196
|
filePath: dir,
|
|
194
197
|
sizeBytes,
|
|
198
|
+
contextLength,
|
|
195
199
|
backend: "mlx-vlm",
|
|
196
200
|
format: "mlx",
|
|
197
201
|
source: sourceLabel,
|
|
@@ -287,4 +291,51 @@ export function defaultMlxContextLength(trainedCtx, ramGb) {
|
|
|
287
291
|
if (ramGb < 16) return Math.min(trainedCtx, 8192);
|
|
288
292
|
if (ramGb < 32) return Math.min(trainedCtx, 16384);
|
|
289
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;
|
|
290
341
|
}
|
package/src/model-catalog.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { loadProfiles, normalizeProfile, sanitizeProfileId } from "./profiles.mj
|
|
|
3
3
|
import { scanManagedModels } from "./managed.mjs";
|
|
4
4
|
import { scanMlxModels } from "./mlx-discovery.mjs";
|
|
5
5
|
import { isProfileFileMissing } from "./model-summary.mjs";
|
|
6
|
+
import { backendFor } from "./backends.mjs";
|
|
6
7
|
|
|
7
8
|
export async function loadModelCatalog() {
|
|
8
9
|
const [profiles, { models: ggufModels, drafters }, managedModels, mlxModels] = await Promise.all([
|
|
@@ -34,7 +35,7 @@ export function normalizeCatalog(catalog) {
|
|
|
34
35
|
if (!profiledAliases.has(`${backendId}:${model.id}`)) managedItems.push({ model, backendId });
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
return { profiles, ggufModels, drafters, managedModels, newModels, managedItems };
|
|
38
|
+
return { profiles, ggufModels, drafters, managedModels, mlxModels, newModels, managedItems };
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function itemKey(item) {
|
|
@@ -56,13 +57,73 @@ function compareRecency(a, b) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export function buildCatalogItems(normalized) {
|
|
59
|
-
const { profiles, newModels, managedItems, drafters } = normalized;
|
|
60
|
-
|
|
60
|
+
const { profiles, newModels, managedItems, drafters, ggufModels = [], mlxModels = [], managedModels = [] } = normalized;
|
|
61
|
+
|
|
62
|
+
// Lookup maps for enriching profile items with scan data (size + context).
|
|
63
|
+
const scanByPath = new Map();
|
|
64
|
+
for (const m of ggufModels) scanByPath.set(m.path, m);
|
|
65
|
+
for (const m of mlxModels) scanByPath.set(m.filePath ?? m.path, m);
|
|
66
|
+
|
|
67
|
+
const managedByKey = new Map();
|
|
68
|
+
for (const { backendId, models } of managedModels) {
|
|
69
|
+
for (const m of models) managedByKey.set(`${backendId}:${m.id}`, m);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const profileItems = profiles.map((profile) => {
|
|
73
|
+
const item = { type: "profile", profile, label: profile.label, fileMissing: isProfileFileMissing(profile) };
|
|
74
|
+
|
|
75
|
+
// Resolve size: profile.modelSizeBytes → scan lookup → managed lookup
|
|
76
|
+
let sizeBytes = profile.modelSizeBytes || 0;
|
|
77
|
+
if (!sizeBytes && profile.modelPath) {
|
|
78
|
+
const scanModel = scanByPath.get(profile.modelPath);
|
|
79
|
+
if (scanModel?.sizeBytes) sizeBytes = scanModel.sizeBytes;
|
|
80
|
+
}
|
|
81
|
+
if (!sizeBytes) {
|
|
82
|
+
const backend = backendFor(profile.backend);
|
|
83
|
+
if (backend.type === "managed-server" && profile.omlxModel) {
|
|
84
|
+
const managedModel = managedByKey.get(`${profile.backend}:${profile.omlxModel}`);
|
|
85
|
+
if (managedModel?.sizeBytes) sizeBytes = managedModel.sizeBytes;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
item.sizeBytes = sizeBytes || null;
|
|
89
|
+
|
|
90
|
+
// Resolve context: flags.ctxSize (configured) → capabilities.ctxSize (trained) → scan → managed
|
|
91
|
+
let contextLength = profile.flags?.ctxSize ?? null;
|
|
92
|
+
if (!contextLength) contextLength = profile.capabilities?.ctxSize ?? null;
|
|
93
|
+
if (!contextLength && profile.modelPath) {
|
|
94
|
+
const scanModel = scanByPath.get(profile.modelPath);
|
|
95
|
+
if (scanModel?.contextLength) contextLength = scanModel.contextLength;
|
|
96
|
+
}
|
|
97
|
+
if (!contextLength) {
|
|
98
|
+
const backend = backendFor(profile.backend);
|
|
99
|
+
if (backend.type === "managed-server" && profile.omlxModel) {
|
|
100
|
+
const managedModel = managedByKey.get(`${profile.backend}:${profile.omlxModel}`);
|
|
101
|
+
if (managedModel?.contextLength) contextLength = managedModel.contextLength;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
item.contextLength = contextLength;
|
|
105
|
+
|
|
106
|
+
return item;
|
|
107
|
+
});
|
|
61
108
|
profileItems.sort(compareRecency);
|
|
62
109
|
return [
|
|
63
110
|
...profileItems,
|
|
64
|
-
...newModels.map((model) => ({
|
|
65
|
-
|
|
111
|
+
...newModels.map((model) => ({
|
|
112
|
+
type: "new",
|
|
113
|
+
model,
|
|
114
|
+
label: model.label,
|
|
115
|
+
drafter: matchDrafter(model.path, drafters),
|
|
116
|
+
sizeBytes: model.sizeBytes || null,
|
|
117
|
+
contextLength: model.contextLength ?? null,
|
|
118
|
+
})),
|
|
119
|
+
...managedItems.map(({ model, backendId }) => ({
|
|
120
|
+
type: "managed",
|
|
121
|
+
model,
|
|
122
|
+
backendId,
|
|
123
|
+
label: model.label,
|
|
124
|
+
sizeBytes: model.sizeBytes || null,
|
|
125
|
+
contextLength: model.contextLength ?? null,
|
|
126
|
+
})),
|
|
66
127
|
];
|
|
67
128
|
}
|
|
68
129
|
|
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,32 +101,18 @@ function discoverySourceForItem(item) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function optionCtxLabel(item) {
|
|
104
|
-
|
|
105
|
-
|
|
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);
|
|
107
|
+
if (item.contextLength) {
|
|
108
|
+
return optionPad(`${(item.contextLength / 1000).toFixed(0)}k`, null, OPTION_CTX_WIDTH);
|
|
106
109
|
}
|
|
107
110
|
return optionPad("—", null, OPTION_CTX_WIDTH);
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
function optionSizeLabel(item
|
|
111
|
-
if (item.type === "profile")
|
|
112
|
-
|
|
113
|
-
if (item.profile.modelSizeBytes) return formatBytes(item.profile.modelSizeBytes);
|
|
114
|
-
if (item.profile.modelPath && existsSync(item.profile.modelPath)) {
|
|
115
|
-
const s = statSync(item.profile.modelPath);
|
|
116
|
-
// Only stat regular files — a modelPath that is a directory (MLX)
|
|
117
|
-
// reports the dir entry size, not the model size.
|
|
118
|
-
if (s.isFile()) return formatBytes(s.size);
|
|
119
|
-
}
|
|
120
|
-
const managedSize = managedProfileSizeBytes(item.profile, managedModels);
|
|
121
|
-
if (managedSize) return formatBytes(managedSize);
|
|
122
|
-
return "—";
|
|
123
|
-
}
|
|
124
|
-
if (item.type === "new") {
|
|
125
|
-
return formatBytes(item.model.sizeBytes);
|
|
126
|
-
}
|
|
127
|
-
// managed
|
|
128
|
-
if (item.model.sizeBytes) return formatBytes(item.model.sizeBytes);
|
|
129
|
-
if (item.model.quant) return item.model.quant;
|
|
113
|
+
function optionSizeLabel(item) {
|
|
114
|
+
if (item.type === "profile" && item.fileMissing) return "—";
|
|
115
|
+
if (item.sizeBytes) return formatBytes(item.sizeBytes);
|
|
130
116
|
return "—";
|
|
131
117
|
}
|
|
132
118
|
|
|
@@ -141,7 +127,7 @@ function optionLabel({ status, backend, source, name, ctx, size, nameWidth }) {
|
|
|
141
127
|
return [status, backend, source, pc.bold(optionPad(name, null, nameWidth)), ctx, pc.dim(size)].join(OPTION_SEPARATOR);
|
|
142
128
|
}
|
|
143
129
|
|
|
144
|
-
export function modelSelectOption(item, { runningProfilesNow, modelMissingIds, nameWidth
|
|
130
|
+
export function modelSelectOption(item, { runningProfilesNow, modelMissingIds, nameWidth }) {
|
|
145
131
|
const sourceId = discoverySourceForItem(item) ?? "unknown";
|
|
146
132
|
const backendId = inferBackendId(item);
|
|
147
133
|
if (item.type === "profile") {
|
|
@@ -162,7 +148,7 @@ export function modelSelectOption(item, { runningProfilesNow, modelMissingIds, n
|
|
|
162
148
|
name: item.profile.label,
|
|
163
149
|
nameWidth,
|
|
164
150
|
ctx: optionCtxLabel(item),
|
|
165
|
-
size: optionSizeLabel(item
|
|
151
|
+
size: optionSizeLabel(item),
|
|
166
152
|
}),
|
|
167
153
|
...(hint ? { hint: pc.red(hint) } : {}),
|
|
168
154
|
};
|
|
@@ -177,7 +163,7 @@ export function modelSelectOption(item, { runningProfilesNow, modelMissingIds, n
|
|
|
177
163
|
name: item.model.label,
|
|
178
164
|
nameWidth,
|
|
179
165
|
ctx: optionCtxLabel(item),
|
|
180
|
-
size: optionSizeLabel(item
|
|
166
|
+
size: optionSizeLabel(item),
|
|
181
167
|
}),
|
|
182
168
|
};
|
|
183
169
|
}
|
|
@@ -190,22 +176,11 @@ export function modelSelectOption(item, { runningProfilesNow, modelMissingIds, n
|
|
|
190
176
|
name: item.model.label,
|
|
191
177
|
nameWidth,
|
|
192
178
|
ctx: optionCtxLabel(item),
|
|
193
|
-
size: optionSizeLabel(item
|
|
179
|
+
size: optionSizeLabel(item),
|
|
194
180
|
}),
|
|
195
181
|
};
|
|
196
182
|
}
|
|
197
183
|
|
|
198
|
-
function managedProfileSizeBytes(profile, managedModels) {
|
|
199
|
-
if (!managedModels || !Array.isArray(managedModels)) return null;
|
|
200
|
-
const backend = backendFor(profile.backend);
|
|
201
|
-
if (backend.type !== "managed-server") return null;
|
|
202
|
-
const backendModels = managedModels.find((m) => m.backendId === profile.backend)?.models ?? [];
|
|
203
|
-
const modelId = profile.omlxModel ?? null;
|
|
204
|
-
if (!modelId) return null;
|
|
205
|
-
const model = backendModels.find((m) => m.id === modelId);
|
|
206
|
-
return model?.sizeBytes || null;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
184
|
function inferBackendId(item) {
|
|
210
185
|
if (item.type === "profile") return item.profile.backend;
|
|
211
186
|
if (item.type === "managed") return item.backendId;
|
|
@@ -266,7 +241,7 @@ export async function printProfileDetails(profile) {
|
|
|
266
241
|
detailRows.push(
|
|
267
242
|
["Local file", fileMissing ? pc.red(`${profile.modelPath} (not found)`) : profile.modelPath ?? "unknown"],
|
|
268
243
|
["Vision file", profile.mmprojPath ? (existsSync(profile.mmprojPath) ? profile.mmprojPath : pc.red(`${profile.mmprojPath} (not found)`)) : "none"],
|
|
269
|
-
["Model size", profile.modelPath && existsSync(profile.modelPath) ? formatBytes(statSync(profile.modelPath).size) : "unknown"],
|
|
244
|
+
["Model size", profile.modelSizeBytes ? formatBytes(profile.modelSizeBytes) : (profile.modelPath && existsSync(profile.modelPath) && statSync(profile.modelPath).isFile() ? formatBytes(statSync(profile.modelPath).size) : "unknown")],
|
|
270
245
|
);
|
|
271
246
|
if (profile.drafterPath) {
|
|
272
247
|
detailRows.push(["Drafter", existsSync(profile.drafterPath) ? profile.drafterPath : pc.red(`${profile.drafterPath} (not found)`)]);
|
package/src/scan.mjs
CHANGED
|
@@ -57,6 +57,9 @@ async function scanOneDir(root, sourceLabel = "local-gguf") {
|
|
|
57
57
|
// Read GGUF metadata to detect drafter architecture and embeddings
|
|
58
58
|
const meta = safeReadGgufMetadata(path);
|
|
59
59
|
const architecture = typeof meta["general.architecture"] === "string" ? meta["general.architecture"] : null;
|
|
60
|
+
const contextLength = architecture && typeof meta[`${architecture}.context_length`] === "number"
|
|
61
|
+
? meta[`${architecture}.context_length`]
|
|
62
|
+
: null;
|
|
60
63
|
|
|
61
64
|
if (isEmbeddingArchitecture(architecture, name)) continue;
|
|
62
65
|
|
|
@@ -81,6 +84,7 @@ async function scanOneDir(root, sourceLabel = "local-gguf") {
|
|
|
81
84
|
aliasSuggestion: parsed.id,
|
|
82
85
|
quant: parsed.quant,
|
|
83
86
|
sizeBytes,
|
|
87
|
+
contextLength,
|
|
84
88
|
backend: "llama-cpp",
|
|
85
89
|
source: sourceLabel,
|
|
86
90
|
});
|