offgrid-ai 0.15.9 → 0.16.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.
@@ -8,13 +8,11 @@ import pc from "picocolors";
8
8
  // ── Pi model id ─────────────────────────────────────────────────────────────
9
9
 
10
10
  /**
11
- * The model id Pi must send in requests. mlx-vlm registers the loaded model
12
- * with the full --model path as its API id (verified via /v1/models); sending
13
- * the repo-id label instead makes mlx-vlm unload the local model and re-fetch
14
- * the repo from HuggingFace. Other backends use the friendly modelAlias.
11
+ * The model id Pi must send in requests. All backends use the friendly
12
+ * modelAlias as the API model id.
15
13
  */
16
14
  export function piApiModelId(profile) {
17
- return profile.backend === "mlx-vlm" ? profile.modelPath : profile.modelAlias;
15
+ return profile.modelAlias;
18
16
  }
19
17
 
20
18
  // ── Sync Pi config ─────────────────────────────────────────────────────────
@@ -87,7 +85,7 @@ async function activeProviderProfiles(currentProfile) {
87
85
  const byAlias = new Map();
88
86
  for (const item of [...allProfiles, currentProfile]) {
89
87
  if (item.providerId !== currentProfile.providerId) continue;
90
- if (item.backend !== "llama-cpp" && item.backend !== "llama-cpp-mtp") {
88
+ if (item.backend !== "llama-cpp") {
91
89
  byAlias.set(item.modelAlias, item);
92
90
  continue;
93
91
  }
@@ -135,7 +133,7 @@ export function modelReasoning(profile) {
135
133
  }
136
134
 
137
135
  export function modelFamily(profile) {
138
- return [profile.id, profile.label, profile.modelAlias, profile.modelPath, profile.omlxModel].filter(Boolean).join(" ").toLowerCase();
136
+ return [profile.id, profile.label, profile.modelAlias, profile.omlxModel].filter(Boolean).join(" ").toLowerCase();
139
137
  }
140
138
 
141
139
  function piApiKey() {
package/src/managed.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { BACKENDS } from "./backends.mjs";
3
- import { commandExists } from "./exec.mjs";
3
+ import { hasOmlx } from "./omlx-runtime.mjs";
4
4
 
5
5
  export const MANAGED_BACKEND_IDS = ["omlx"];
6
6
 
@@ -22,6 +22,6 @@ export function hasLmStudioInstalled() {
22
22
  return existsSync("/Applications/LM Studio.app");
23
23
  }
24
24
 
25
- export function hasOmlxInstalled() {
26
- return commandExists("omlx");
25
+ export async function hasOmlxInstalled() {
26
+ return await hasOmlx();
27
27
  }
@@ -1,36 +1,14 @@
1
- // MLX model discovery + metadata — scans configured model directories for MLX
2
- // model directories and parses their config.json.
3
- // Ported from deprecated-offgrid-desktop/src/main/model-discovery.ts +
4
- // mlx-metadata.ts (MLX subset only).
5
- //
6
- // This runs ALONGSIDE offgrid-ai's existing GGUF scan (scan.mjs scanGgufModels)
7
- // — it does not replace it. The picker (main.mjs) will merge GGUF + MLX lists.
8
- //
9
- // An MLX model directory is one containing config.json + one or more
10
- // *.safetensors files. HuggingFace Hub cache layout (models--org--name) is
11
- // detected and scanned specially.
1
+ // oMLX model size lookup — scans ~/.omlx/models/ for MLX model directories
2
+ // to compute sizes and publishers. The oMLX API doesn't return these, so we
3
+ // read them from disk.
12
4
 
13
5
  import { readdir, stat, readFile } from "node:fs/promises";
14
6
  import { existsSync } from "node:fs";
15
- import { join, basename } from "node:path";
7
+ import { join } from "node:path";
16
8
  import { homedir } from "node:os";
17
- import { getModelScanDirs } from "./config.mjs";
18
- import { inferSourceLabel, MIN_MODEL_SIZE_BYTES, EMBEDDING_MODEL_TYPES } from "./discovery-shared.mjs";
19
- import { parseModelName } from "./model-name.mjs";
20
9
 
21
- // ── Folder → backend mapping ──────────────────────────────────────────────
22
- // The oMLX folder is oMLX-exclusive: models there are served by the oMLX
23
- // managed backend, NOT by mlx-vlm. Every OTHER scan dir is format-based
24
- // (GGUF → llama.cpp, MLX → mlx-vlm). So mlx-vlm scans all configured dirs
25
- // EXCEPT the oMLX folder.
26
10
  const OMLX_MODELS_DIR = join(homedir(), ".omlx", "models");
27
- function isOmlxFolder(p) {
28
- return p === OMLX_MODELS_DIR || p.startsWith(OMLX_MODELS_DIR + "/");
29
- }
30
-
31
- // ── MLX directory detection ───────────────────────────────────────────────
32
11
 
33
- /** True if dir contains config.json + at least one .safetensors file. */
34
12
  async function isMlxModelDir(dir) {
35
13
  if (!existsSync(join(dir, "config.json"))) return false;
36
14
  try {
@@ -41,7 +19,6 @@ async function isMlxModelDir(dir) {
41
19
  }
42
20
  }
43
21
 
44
- /** Sum the size of all .safetensors files in an MLX model dir (bytes). */
45
22
  async function getMlxDirSizeBytes(dir) {
46
23
  try {
47
24
  const entries = await readdir(dir);
@@ -57,290 +34,132 @@ async function getMlxDirSizeBytes(dir) {
57
34
  }
58
35
  }
59
36
 
60
- // ── Recursive MLX scanner ─────────────────────────────────────────────────
61
-
62
37
  /**
63
- * Recursively scan a directory for MLX model directories.
64
- * Searches up to maxDepth levels deep. Does NOT collect GGUF (that's scan.mjs).
38
+ * Scan ~/.omlx/models/ for MLX model directories and return a Map of
39
+ * basename { sizeBytes, publisher }.
65
40
  */
66
- async function scanDirRecursiveForMlx(rootDir, sourceLabel, maxDepth = 3) {
67
- if (!existsSync(rootDir)) return [];
68
- const models = [];
41
+ export async function scanOmlxModelSizes() {
42
+ if (!existsSync(OMLX_MODELS_DIR)) return new Map();
43
+ const infoByBasename = new Map();
69
44
 
70
- async function walk(dir, depth) {
71
- if (depth > maxDepth) return;
45
+ async function walk(dir, publisher) {
72
46
  let entries;
73
47
  try {
74
48
  entries = await readdir(dir, { withFileTypes: true });
75
49
  } catch {
76
50
  return;
77
51
  }
78
-
79
- // Is this directory itself an MLX model dir? (don't recurse into it)
80
- if (depth > 0 && await isMlxModelDir(dir)) {
81
- const sizeBytes = await getMlxDirSizeBytes(dir);
82
- if (sizeBytes < MIN_MODEL_SIZE_BYTES) return;
83
- if (await isEmbeddingMlxModel(join(dir, "config.json"))) return;
84
- const caps = await detectMlxCapabilities(dir);
85
- const { display, quant } = parseModelName(basename(dir), sourceLabel);
86
- models.push(makeMlxModel(dir, display, sizeBytes, sourceLabel, rootDir, caps.contextLength, quant));
87
- return;
88
- }
89
-
90
52
  for (const entry of entries) {
91
- if (entry.name.startsWith(".") || entry.name === "README.md" || entry.name === ".gitattributes") continue;
53
+ if (!entry.isDirectory()) continue;
92
54
  const fullPath = join(dir, entry.name);
93
- if (entry.isDirectory()) {
94
- if (await isMlxModelDir(fullPath)) {
95
- const sizeBytes = await getMlxDirSizeBytes(fullPath);
96
- if (sizeBytes < MIN_MODEL_SIZE_BYTES) continue;
97
- if (await isEmbeddingMlxModel(join(fullPath, "config.json"))) continue;
98
- const caps = await detectMlxCapabilities(fullPath);
99
- // Extract publisher from parent dir (LM Studio: publisher/model-dir)
100
- const relParts = fullPath.slice(rootDir.length + 1).split("/");
101
- const publisher = (sourceLabel === "lmstudio" && relParts.length >= 2) ? relParts[0] : null;
102
- const rawLabel = publisher ? `${publisher}/${entry.name}` : entry.name;
103
- const { display, quant } = parseModelName(rawLabel, sourceLabel);
104
- models.push(makeMlxModel(fullPath, display, sizeBytes, sourceLabel, rootDir, caps.contextLength, quant));
105
- } else {
106
- await walk(fullPath, depth + 1);
107
- }
55
+ if (await isMlxModelDir(fullPath)) {
56
+ const sizeBytes = await getMlxDirSizeBytes(fullPath);
57
+ if (sizeBytes > 0) infoByBasename.set(entry.name, { sizeBytes, publisher });
58
+ } else {
59
+ await walk(fullPath, publisher ?? entry.name);
108
60
  }
109
61
  }
110
62
  }
111
63
 
112
- await walk(rootDir, 0);
113
- return models;
64
+ await walk(OMLX_MODELS_DIR, null);
65
+ return infoByBasename;
114
66
  }
115
67
 
116
- // ── HuggingFace Hub layout ────────────────────────────────────────────────
68
+ // ── MTP capability detection ─────────────────────────────────────────────
69
+ // oMLX supports native MTP for Qwen 3.5/3.6 (dense + MoE) and DeepSeek-V4
70
+ // models that ship MTP heads in their weights. The check mirrors oMLX's own
71
+ // _mtp_compat_for_model: config must declare mtp_num_hidden_layers, model_type
72
+ // must be on the whitelist, and the safetensors index must contain mtp.* keys.
117
73
 
118
- /** True if dir looks like an HF Hub cache (has models--* subdirs). */
119
- async function looksLikeHfHub(dir) {
120
- if (!existsSync(dir)) return false;
121
- try {
122
- const entries = await readdir(dir, { withFileTypes: true });
123
- return entries.some((e) => e.isDirectory() && e.name.startsWith("models--"));
124
- } catch {
125
- return false;
126
- }
127
- }
74
+ const MTP_MODEL_TYPES = ["qwen3_5", "qwen3_5_moe", "qwen3_6", "qwen3_6_moe", "deepseek_v4"];
128
75
 
129
76
  /**
130
- * Scan an HF Hub cache dir for MLX model dirs.
131
- * HF layout: models--org--name/snapshots/hash/files
77
+ * Check if an oMLX model directory supports native MTP.
78
+ * Returns { compatible: boolean, reason: string }.
132
79
  */
133
- async function scanHfHubForMlx(dir, sourceLabel) {
134
- if (!existsSync(dir)) return [];
135
- const models = [];
136
- try {
137
- const entries = await readdir(dir, { withFileTypes: true });
138
- for (const entry of entries) {
139
- if (!entry.isDirectory() || !entry.name.startsWith("models--")) continue;
140
- const parts = entry.name.slice("models--".length).split("--");
141
- const label = parts.join("/");
142
- const snapshotsDir = join(dir, entry.name, "snapshots");
143
- if (!existsSync(snapshotsDir)) continue;
144
- const snapshots = await readdir(snapshotsDir, { withFileTypes: true });
145
- // Follow symlinks (HF hub uses them; test imports use them too). A model
146
- // dir can have several snapshots — some incomplete/empty. Check EACH
147
- // snapshot and use the first that is a valid MLX model dir, rather than
148
- // giving up on the whole model if the first snapshot happens to be empty.
149
- const candidates = snapshots.filter((s) => s.isDirectory() || s.isSymbolicLink());
150
- let snapshotPath = null;
151
- for (const snap of candidates) {
152
- const sp = join(snapshotsDir, snap.name);
153
- const st = await stat(sp).catch(() => null);
154
- if (st?.isDirectory() && await isMlxModelDir(sp)) { snapshotPath = sp; break; }
155
- }
156
-
157
- if (!snapshotPath) continue;
158
- const sizeBytes = await getMlxDirSizeBytes(snapshotPath);
159
- if (sizeBytes < MIN_MODEL_SIZE_BYTES) continue;
160
- if (await isEmbeddingMlxModel(join(snapshotPath, "config.json"))) continue;
161
- const caps = await detectMlxCapabilities(snapshotPath);
162
- const { display, quant } = parseModelName(label, sourceLabel);
163
- models.push({
164
- id: `${sourceLabel}:${entry.name}`,
165
- label: display,
166
- path: snapshotPath,
167
- filePath: snapshotPath,
168
- sizeBytes,
169
- contextLength: caps.contextLength,
170
- quant,
171
- backend: "mlx-vlm",
172
- format: "mlx",
173
- source: sourceLabel,
174
- });
175
- }
176
- } catch {
177
- // Can't read — return what we have.
178
- }
179
- return models;
180
- }
181
-
182
- // ── Embedding model filtering for MLX ─────────────────────────────────────
80
+ export async function detectOmlxMtpCapability(modelDir) {
81
+ const configPath = join(modelDir, "config.json");
82
+ if (!existsSync(configPath)) return { compatible: false, reason: "config.json not found" };
183
83
 
184
- async function isEmbeddingMlxModel(configPath) {
185
- if (!existsSync(configPath)) return false;
84
+ let config;
186
85
  try {
187
- const config = JSON.parse(await readFile(configPath, "utf-8"));
188
- const textConfig = config.text_config ?? config;
189
- const modelType = String(textConfig.model_type ?? "").toLowerCase();
190
- if (EMBEDDING_MODEL_TYPES.has(modelType)) return true;
191
- const arch = Array.isArray(config.architectures) ? config.architectures[0] : "";
192
- const lowerArch = String(arch).toLowerCase();
193
- return EMBEDDING_MODEL_TYPES.has(lowerArch) || lowerArch.includes("bert");
86
+ config = JSON.parse(await readFile(configPath, "utf8"));
194
87
  } catch {
195
- return false;
88
+ return { compatible: false, reason: "failed to read config.json" };
196
89
  }
197
- }
198
90
 
199
- // ── MLX model entry builder ───────────────────────────────────────────────
91
+ const mtpLayers = config.mtp_num_hidden_layers;
92
+ if (!mtpLayers || mtpLayers <= 0) {
93
+ return { compatible: false, reason: "model has no MTP heads in config" };
94
+ }
200
95
 
201
- function makeMlxModel(dir, label, sizeBytes, sourceLabel, rootDir, contextLength = null, quant = null) {
202
- return {
203
- id: `${sourceLabel}:${dir.replace(rootDir + "/", "")}`,
204
- label,
205
- path: dir,
206
- filePath: dir,
207
- sizeBytes,
208
- contextLength,
209
- quant,
210
- backend: "mlx-vlm",
211
- format: "mlx",
212
- source: sourceLabel,
213
- };
214
- }
96
+ const modelType = config.model_type;
97
+ if (!MTP_MODEL_TYPES.some((t) => modelType === t || modelType?.startsWith(t))) {
98
+ return { compatible: false, reason: `model_type=${modelType} is not on the MTP whitelist (supported: ${MTP_MODEL_TYPES.join(", ")})` };
99
+ }
215
100
 
216
- // ── Public API ─────────────────────────────────────────────────────────────
101
+ // Check for mtp.* weight tensors in the safetensors index
102
+ const hasWeights = await modelHasMtpWeights(modelDir);
103
+ if (!hasWeights) {
104
+ return { compatible: false, reason: "Config declares MTP layers but the converted weights are missing mtp.* tensors. Re-convert from HF with a converter that preserves MTP weights." };
105
+ }
217
106
 
218
- /**
219
- * Discover all MLX models across the configured scan directories.
220
- * Reads scan dirs from config.mjs getModelScanDirs() — same paths GGUF uses
221
- * (LM Studio, HF hub, user-added). Returns a flat, deduplicated list.
222
- */
223
- export async function scanMlxModels(dirs) {
224
- // mlx-vlm scans every configured dir EXCEPT the oMLX folder (oMLX-exclusive).
225
- const scanDirs = (dirs ?? await getModelScanDirs()).filter((d) => !isOmlxFolder(d));
226
- const results = await Promise.all(
227
- scanDirs.map(async (dir) => {
228
- const label = inferSourceLabel(dir);
229
- if (await looksLikeHfHub(dir)) return scanHfHubForMlx(dir, label);
230
- return scanDirRecursiveForMlx(dir, label);
231
- }),
232
- );
233
- const all = results.flat();
234
- // Deduplicate by filePath (same model may appear in multiple paths).
235
- const seen = new Set();
236
- return all.filter((m) => {
237
- if (seen.has(m.filePath)) return false;
238
- seen.add(m.filePath);
239
- return true;
240
- });
107
+ return { compatible: true, reason: "" };
241
108
  }
242
109
 
243
- // ── MLX capability detection ─────────────────────────────────────────────
244
-
245
- /**
246
- * Detect MLX model capabilities from its config.json.
247
- * Returns { architecture, thinking, vision, contextLength }.
248
- */
249
- export async function detectMlxCapabilities(modelDir) {
250
- const configPath = join(modelDir, "config.json");
251
- if (!existsSync(configPath)) return { thinking: false, vision: false, contextLength: null, architecture: null };
110
+ async function modelHasMtpWeights(modelDir) {
111
+ const indexPath = join(modelDir, "model.safetensors.index.json");
112
+ if (existsSync(indexPath)) {
113
+ try {
114
+ const index = JSON.parse(await readFile(indexPath, "utf8"));
115
+ const weightMap = index.weight_map ?? {};
116
+ return Object.keys(weightMap).some((key) => key.includes("mtp."));
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+ // Single-shard fallback: can't easily read safetensors keys in Node without
122
+ // the safetensors library. Check for an mtp-specific safetensors file.
252
123
  try {
253
- const config = JSON.parse(await readFile(configPath, "utf-8"));
254
- return detectMlxCapabilitiesFromConfig(config, modelDir);
124
+ const entries = await readdir(modelDir);
125
+ return entries.some((f) => f.endsWith(".safetensors") && /mtp/i.test(f));
255
126
  } catch {
256
- return { thinking: false, vision: false, contextLength: null, architecture: null };
127
+ return false;
257
128
  }
258
129
  }
259
130
 
260
- export function detectMlxCapabilitiesFromConfig(config, modelDir) {
261
- const textConfig = config.text_config ?? config;
262
- const rawName = config._name_or_path ?? basename(modelDir ?? "");
263
- const name = String(rawName).toLowerCase();
264
- const label = String(rawName);
265
-
266
- const modelType = String(config.model_type ?? "").toLowerCase();
267
- const textModelType = String(textConfig.model_type ?? "").toLowerCase();
268
-
269
- const vision = Boolean(
270
- config.vision_config ||
271
- config.image_token_id != null ||
272
- config.video_token_id != null ||
273
- config.vision_start_token_id != null ||
274
- modelType.includes("vl") ||
275
- modelType.includes("vision") ||
276
- textModelType.includes("vl") ||
277
- textModelType.includes("vision")
278
- );
279
-
280
- const thinking = /qwen3|gemma-4|gemma4|deepseek-r[12]/i.test(name + " " + label);
281
-
282
- const architectures = Array.isArray(config.architectures) ? config.architectures : [];
283
- const architecture = architectures[0] ?? null;
284
-
285
- const candidates = [
286
- textConfig.max_position_embeddings,
287
- textConfig.sliding_window,
288
- config.max_position_embeddings,
289
- config.sliding_window,
290
- ].filter((v) => typeof v === "number" && v > 0);
291
- const contextLength = candidates.length > 0 ? Math.max(...candidates) : null;
292
-
293
- return { thinking, vision, contextLength, architecture };
294
- }
295
-
296
- /**
297
- * Pick a sensible default context length for an MLX model, capping by RAM.
298
- */
299
- export function defaultMlxContextLength(trainedCtx, ramGb) {
300
- if (!trainedCtx || trainedCtx <= 0) return 8192;
301
- if (ramGb < 12) return Math.min(trainedCtx, 4096);
302
- if (ramGb < 16) return Math.min(trainedCtx, 8192);
303
- if (ramGb < 32) return Math.min(trainedCtx, 16384);
304
- return trainedCtx;
305
- }
306
-
307
- // ── oMLX model size lookup (from disk) ────────────────────────────────────
308
-
309
131
  /**
310
- * Scan the oMLX models directory (~/.omlx/models/) for MLX model directories
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.
132
+ * Find the model directory for a given oMLX model ID.
133
+ * Searches ~/.omlx/models/ recursively for a directory matching the model ID.
313
134
  */
314
- export async function scanOmlxModelSizes() {
315
- if (!existsSync(OMLX_MODELS_DIR)) return new Map();
316
- const infoByBasename = new Map();
135
+ export async function findOmlxModelDir(modelId) {
136
+ if (!existsSync(OMLX_MODELS_DIR)) return null;
137
+ const basename = modelId.includes("/") ? modelId.slice(modelId.lastIndexOf("/") + 1)
138
+ : modelId.includes("--") ? modelId.slice(modelId.lastIndexOf("--") + 2)
139
+ : modelId;
317
140
 
318
- async function walk(dir, publisher) {
141
+ async function walk(dir) {
319
142
  let entries;
320
143
  try {
321
144
  entries = await readdir(dir, { withFileTypes: true });
322
145
  } catch {
323
- return;
146
+ return null;
324
147
  }
325
148
  for (const entry of entries) {
326
149
  if (!entry.isDirectory()) continue;
327
150
  const fullPath = join(dir, entry.name);
328
- if (await isMlxModelDir(fullPath)) {
329
- const sizeBytes = await getMlxDirSizeBytes(fullPath);
330
- if (sizeBytes > 0) infoByBasename.set(entry.name, { sizeBytes, publisher });
331
- } else {
332
- // First-level directories under ~/.omlx/models/ are publishers
333
- await walk(fullPath, publisher ?? entry.name);
334
- }
151
+ if (entry.name === basename && await isMlxModelDir(fullPath)) return fullPath;
152
+ const found = await walk(fullPath);
153
+ if (found) return found;
335
154
  }
155
+ return null;
336
156
  }
337
157
 
338
- await walk(OMLX_MODELS_DIR, null);
339
- return infoByBasename;
158
+ return await walk(OMLX_MODELS_DIR);
340
159
  }
341
160
 
342
161
  /**
343
- * Look up a model's info by its oMLX API id. Tries exact match, then the
162
+ * Look up a model's info by its oMLX API id. Tries exact match, then the
344
163
  * segment after `--` (oMLX org--name format), then after `/` (HF format).
345
164
  */
346
165
  export function lookupOmlxModelInfo(modelId, infoMap) {
@@ -1,28 +1,23 @@
1
1
  import { scanGgufModels, matchDrafter } from "./scan.mjs";
2
2
  import { loadProfiles, normalizeProfile, sanitizeProfileId } from "./profiles.mjs";
3
3
  import { scanManagedModels } from "./managed.mjs";
4
- import { scanMlxModels } from "./mlx-discovery.mjs";
5
4
  import { isProfileFileMissing } from "./model-summary.mjs";
6
5
  import { backendFor } from "./backends.mjs";
7
6
 
8
7
  export async function loadModelCatalog() {
9
- const [profiles, { models: ggufModels, drafters }, managedModels, mlxModels] = await Promise.all([
8
+ const [profiles, { models: ggufModels, drafters }, managedModels] = await Promise.all([
10
9
  loadProfiles(),
11
10
  scanGgufModels(),
12
11
  scanManagedModels(),
13
- scanMlxModels(),
14
12
  ]);
15
- return normalizeCatalog({ profiles, ggufModels, drafters, managedModels, mlxModels });
13
+ return normalizeCatalog({ profiles, ggufModels, drafters, managedModels });
16
14
  }
17
15
 
18
16
  export function normalizeCatalog(catalog) {
19
17
  if (catalog.newModels && catalog.managedItems) return catalog;
20
- const { profiles, ggufModels, drafters, managedModels, mlxModels = [] } = catalog;
18
+ const { profiles, ggufModels, drafters, managedModels } = catalog;
21
19
  const profiledPaths = new Set(profiles.map((profile) => profile.modelPath).filter(Boolean));
22
- const newModels = [
23
- ...ggufModels.filter((model) => !profiledPaths.has(model.path)),
24
- ...mlxModels.filter((model) => !profiledPaths.has(model.path)),
25
- ];
20
+ const newModels = ggufModels.filter((model) => !profiledPaths.has(model.path));
26
21
  const managedItems = [];
27
22
  for (const { backendId, models, status } of managedModels) {
28
23
  if (status === "unavailable") continue;
@@ -35,9 +30,10 @@ export function normalizeCatalog(catalog) {
35
30
  if (!profiledAliases.has(`${backendId}:${model.id}`)) managedItems.push({ model, backendId });
36
31
  }
37
32
  }
38
- return { profiles, ggufModels, drafters, managedModels, mlxModels, newModels, managedItems };
33
+ return { profiles, ggufModels, drafters, managedModels, newModels, managedItems };
39
34
  }
40
35
 
36
+
41
37
  export function itemKey(item) {
42
38
  if (item.type === "profile") return `profile:${item.profile.id}`;
43
39
  if (item.type === "new") return `new:${item.model.path}`;
@@ -57,12 +53,11 @@ function compareRecency(a, b) {
57
53
  }
58
54
 
59
55
  export function buildCatalogItems(normalized) {
60
- const { profiles, newModels, managedItems, drafters, ggufModels = [], mlxModels = [], managedModels = [] } = normalized;
56
+ const { profiles, newModels, managedItems, drafters, ggufModels = [], managedModels = [] } = normalized;
61
57
 
62
58
  // Lookup maps for enriching profile items with scan data (size + context).
63
59
  const scanByPath = new Map();
64
60
  for (const m of ggufModels) scanByPath.set(m.path, m);
65
- for (const m of mlxModels) scanByPath.set(m.filePath ?? m.path, m);
66
61
 
67
62
  const managedByKey = new Map();
68
63
  for (const { backendId, models } of managedModels) {
@@ -77,7 +72,7 @@ export function buildCatalogItems(normalized) {
77
72
  if (profile.modelPath) {
78
73
  const scanModel = scanByPath.get(profile.modelPath);
79
74
  if (scanModel) {
80
- item.label = scanModel.label; // re-parsed label (publisher/model-name)
75
+ item.label = scanModel.label;
81
76
  if (scanModel.quant) quant = scanModel.quant;
82
77
  }
83
78
  }
@@ -160,4 +155,4 @@ export function createManagedProfile(model, backendId) {
160
155
  modelSizeBytes: model.sizeBytes || 0,
161
156
  ...(backendId === "omlx" ? { omlxModel: model.id } : {}),
162
157
  });
163
- }
158
+ }
@@ -44,8 +44,6 @@ function optionSourceTag(sourceId) {
44
44
  omlx: pc.magenta,
45
45
  "llama.cpp": pc.cyan,
46
46
  gguf: pc.cyan,
47
- mlx: pc.yellow,
48
- "mlx-vlm": pc.yellow,
49
47
  };
50
48
  return optionPad(label, colors[sourceId] ?? pc.dim, OPTION_SOURCE_WIDTH);
51
49
  }
@@ -55,9 +53,7 @@ function optionBackendTag(backendId) {
55
53
  const label = backend?.label ?? backendId ?? "unknown";
56
54
  const colors = {
57
55
  "llama-cpp": pc.cyan,
58
- "llama-cpp-mtp": pc.blue,
59
56
  omlx: pc.magenta,
60
- "mlx-vlm": pc.yellow,
61
57
  };
62
58
  return optionPad(label, colors[backendId] ?? pc.dim, OPTION_BACKEND_WIDTH);
63
59
  }
@@ -70,8 +66,6 @@ export function formatSourceLabel(sourceId) {
70
66
  omlx: "oMLX",
71
67
  "llama.cpp": "llama.cpp",
72
68
  gguf: "GGUF file",
73
- mlx: "MLX",
74
- "mlx-vlm": "MLX",
75
69
  };
76
70
  return map[sourceId] ?? String(sourceId);
77
71
  }
@@ -200,7 +194,6 @@ export function inferBackendId(item) {
200
194
  if (item.type === "profile") return item.profile.backend;
201
195
  if (item.type === "managed") return item.backendId;
202
196
  // new model: derive from format
203
- if (item.model?.format === "mlx") return "mlx-vlm";
204
197
  if (item.model?.backend) return item.model.backend;
205
198
  return "llama-cpp";
206
199
  }
@@ -297,29 +290,6 @@ export function printGgufModelDetails(model, drafter) {
297
290
  console.log("\n" + renderSectionRows("Model details", detailRows, { columns: Math.min(process.stdout.columns ?? 110, 140) }));
298
291
  }
299
292
 
300
- export async function printMlxModelDetails(model) {
301
- const { detectMlxCapabilities } = await import("./mlx-discovery.mjs");
302
- const caps = await detectMlxCapabilities(model.filePath ?? model.path);
303
- const parts = [];
304
- if (caps.architecture) parts.push(caps.architecture);
305
- if (caps.thinking) parts.push("thinking");
306
- if (caps.vision) parts.push("vision");
307
- const summary = parts.length > 0 ? parts.join(pc.dim(" · ")) : "standard MLX";
308
- console.log("\n" + renderSectionRows("Downloaded model", [
309
- ["Name", pc.bold(model.label)],
310
- ["Status", pc.yellow("Needs one-time setup")],
311
- ["Details", summary],
312
- ]));
313
- console.log("\n" + renderSectionRows("Model details", [
314
- ["Model dir", model.path],
315
- ["Backend", "mlx-vlm"],
316
- ["Source", formatSourceLabel(model.source)],
317
- ["Detected", summary],
318
- ["Size", formatBytes(model.sizeBytes)],
319
- ["Context", caps.contextLength ? `${caps.contextLength.toLocaleString()} trained` : "unknown"],
320
- ], { columns: Math.min(process.stdout.columns ?? 110, 140) }));
321
- }
322
-
323
293
  export function printManagedModelDetails(model, backend) {
324
294
  console.log("\n" + renderSectionRows(`${backend.label} model`, [
325
295
  ["Name", pc.bold(model.label)],