offgrid-ai 0.14.0 → 0.14.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
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
@@ -99,8 +99,17 @@ async function scanOmlxModels() {
99
99
  // The oMLX API doesn't return model sizes or publishers — look them up from disk.
100
100
  const infoMap = await scanOmlxModelSizes();
101
101
 
102
- return body.data
103
- .filter((model) => isChatOmlxModel(model))
102
+ // The oMLX API can return the same model twice — once loaded (with
103
+ // max_model_len) and once available (without). Deduplicate by ID,
104
+ // keeping the entry with context window info.
105
+ const byId = new Map();
106
+ for (const model of body.data.filter(isChatOmlxModel)) {
107
+ const existing = byId.get(model.id);
108
+ if (existing && existing.max_model_len) continue; // keep loaded entry
109
+ byId.set(model.id, model);
110
+ }
111
+
112
+ return Array.from(byId.values())
104
113
  .map((model) => {
105
114
  const info = lookupOmlxModelInfo(model.id, infoMap);
106
115
  // If the API ID doesn't already include a publisher (no / or --),
@@ -128,6 +128,28 @@ export async function runPreparedBenchmark(profile, runDirectory, options = {})
128
128
  return metadata;
129
129
  }
130
130
 
131
+ // ── Shared benchmark selection ────────────────────────────────────────────
132
+
133
+ async function selectBenchmark(prompt, repoPath) {
134
+ const kind = await prompt.choice("Benchmark category", [
135
+ { value: "visual", label: "Visual Benchmark", hint: "HTML/CSS/JS animation benchmarks" },
136
+ { value: "data-science", label: "Data Science", hint: "Analysis and charting benchmarks" },
137
+ ], "visual");
138
+
139
+ const benchDir = join(repoPath, "benchmarks");
140
+ const benchmarks = (await loadBenchmarks(benchDir)).filter((b) => b.kind === kind);
141
+ if (benchmarks.length === 0) {
142
+ console.log(pc.yellow(`No ${kind} benchmarks found in ${benchDir}`));
143
+ return null;
144
+ }
145
+ const benchmarkId = await prompt.choice("Prompt", benchmarks.map((b) => ({
146
+ value: b.id, label: b.title, hint: b.description || b.id,
147
+ })), benchmarks[0].id);
148
+ const benchmark = benchmarks.find((b) => b.id === benchmarkId);
149
+ if (!benchmark) return null;
150
+ return { kind, benchmark };
151
+ }
152
+
131
153
  // ── Benchmark from a selected profile (from model picker) ────────────────
132
154
 
133
155
  export async function benchmarkForProfile(profile) {
@@ -137,22 +159,9 @@ export async function benchmarkForProfile(profile) {
137
159
  const repoPath = await linkBenchmarkRepo(prompt);
138
160
  if (!repoPath) return;
139
161
 
140
- const kind = await prompt.choice("Benchmark category", [
141
- { value: "visual", label: "Visual Benchmark", hint: "HTML/CSS/JS animation benchmarks" },
142
- { value: "data-science", label: "Data Science", hint: "Analysis and charting benchmarks" },
143
- ], "visual");
144
-
145
- const benchDir = join(repoPath, "benchmarks");
146
- const benchmarks = (await loadBenchmarks(benchDir)).filter((b) => b.kind === kind);
147
- if (benchmarks.length === 0) {
148
- console.log(pc.yellow(`No ${kind} benchmarks found in ${benchDir}`));
149
- return;
150
- }
151
- const benchmarkId = await prompt.choice("Prompt", benchmarks.map((b) => ({
152
- value: b.id, label: b.title, hint: b.description || b.id,
153
- })), benchmarks[0].id);
154
- const selectedBenchmark = benchmarks.find((b) => b.id === benchmarkId);
155
- if (!selectedBenchmark) return;
162
+ const selected = await selectBenchmark(prompt, repoPath);
163
+ if (!selected) return;
164
+ const { kind, benchmark: selectedBenchmark } = selected;
156
165
 
157
166
  const modelId = profile.modelAlias;
158
167
  const modelSource = benchmarkModelSource(profile);
@@ -177,28 +186,14 @@ export async function benchmarkForProfile(profile) {
177
186
 
178
187
  export async function benchmarkFlow() {
179
188
  await ensureDirs();
180
-
181
189
  const prompt = createPrompt();
182
190
  try {
183
191
  const repoPath = await linkBenchmarkRepo(prompt);
184
192
  if (!repoPath) return;
185
193
 
186
- const kind = await prompt.choice("Benchmark category", [
187
- { value: "visual", label: "Visual Benchmark", hint: "HTML/CSS/JS animation benchmarks" },
188
- { value: "data-science", label: "Data Science", hint: "Analysis and charting benchmarks" },
189
- ], "visual");
190
-
191
- const benchDir = join(repoPath, "benchmarks");
192
- const benchmarks = (await loadBenchmarks(benchDir)).filter((b) => b.kind === kind);
193
- if (benchmarks.length === 0) {
194
- console.log(pc.yellow(`No ${kind} benchmarks found in ${benchDir}`));
195
- return;
196
- }
197
- const benchmarkId = await prompt.choice("Prompt", benchmarks.map((b) => ({
198
- value: b.id, label: b.title, hint: b.description || b.id,
199
- })), benchmarks[0].id);
200
- const selectedBenchmark = benchmarks.find((b) => b.id === benchmarkId);
201
- if (!selectedBenchmark) return;
194
+ const selected = await selectBenchmark(prompt, repoPath);
195
+ if (!selected) return;
196
+ const { kind, benchmark: selectedBenchmark } = selected;
202
197
 
203
198
  const profiles = await loadProfiles();
204
199
  const source = await prompt.choice("Model source", [
@@ -3,7 +3,7 @@
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
  import { pc, renderRows, renderSection } from "../ui.mjs";
6
- import { slugModelId, createRunId, buildToolPrompt } from "./shared.mjs";
6
+ import { slugModelId, createRunId } from "./shared.mjs";
7
7
  import { parseModelName } from "../model-name.mjs";
8
8
 
9
9
  function harnessDisplayName(id) {
@@ -30,7 +30,7 @@ function printBenchmarkNextSteps({ repoPath, runDirectory, profile, modelId, run
30
30
  }
31
31
 
32
32
  export async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSource, backendLabel, profile, showNextSteps = true }) {
33
- const toolPrompt = buildToolPrompt(benchmark);
33
+ const toolPrompt = benchmark.prompt;
34
34
  const now = new Date();
35
35
  const runId = createRunId(now);
36
36
  const modelSlug = slugModelId(modelId);
@@ -5,8 +5,8 @@ import { join, relative, basename } from "node:path";
5
5
  import { Agent } from "@earendil-works/pi-agent-core";
6
6
  import { streamSimple } from "@earendil-works/pi-ai/compat";
7
7
  import { createCodingTools } from "@earendil-works/pi-coding-agent";
8
- import { pc } from "../ui.mjs";
9
- import { piApiModelId } from "../harness-pi.mjs";
8
+ import { pc, formatBytes } from "../ui.mjs";
9
+ import { piApiModelId, modelReasoning, modelCompat } from "../harness-pi.mjs";
10
10
 
11
11
  const C = {
12
12
  thinking: pc.magenta,
@@ -257,16 +257,8 @@ export async function runBenchmarkInPi(profile, runDirectory, { signal } = {}) {
257
257
  // ── Model construction ──────────────────────────────────────────────────────
258
258
 
259
259
  function buildModel(profile) {
260
- const text = [profile.id, profile.label, profile.modelAlias, profile.modelPath, profile.omlxModel]
261
- .filter(Boolean).join(" ").toLowerCase();
262
- const reasoning = profile.reasoning !== undefined
263
- ? Boolean(profile.reasoning)
264
- : /qwen|gemma-4|gemma 4/i.test(text);
265
- const hasCompat = profile.compat
266
- ? profile.compat
267
- : /qwen|gemma-4|gemma 4/i.test(text)
268
- ? { thinkingFormat: "qwen-chat-template" }
269
- : null;
260
+ const reasoning = modelReasoning(profile) ?? false;
261
+ const compat = modelCompat(profile);
270
262
 
271
263
  return {
272
264
  id: piApiModelId(profile),
@@ -283,7 +275,7 @@ function buildModel(profile) {
283
275
  supportsDeveloperRole: false,
284
276
  supportsReasoningEffort: false,
285
277
  maxTokensField: "max_tokens",
286
- ...(hasCompat ?? {}),
278
+ ...(compat ?? {}),
287
279
  },
288
280
  };
289
281
  }
@@ -364,15 +356,6 @@ function truncateOneLine(value, max = 80) {
364
356
  return text.length > max ? `${text.slice(0, Math.max(1, max - 1))}…` : text;
365
357
  }
366
358
 
367
- function formatBytes(bytes) {
368
- if (!Number.isFinite(bytes)) return "unknown";
369
- const units = ["B", "KB", "MB", "GB", "TB"];
370
- let size = bytes;
371
- let unit = 0;
372
- while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit += 1; }
373
- return `${size.toFixed(unit === 0 ? 0 : 2)} ${units[unit]}`;
374
- }
375
-
376
359
  function formatTokens(n) {
377
360
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
378
361
  if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
@@ -18,10 +18,6 @@ export function createRunId(date = new Date()) {
18
18
  return date.toISOString().replace(/:/gu, "-").replace(/\./gu, "-");
19
19
  }
20
20
 
21
- export function buildToolPrompt(benchmark) {
22
- return benchmark.prompt;
23
- }
24
-
25
21
  export async function loadBenchmarks(benchDir) {
26
22
  const entries = await readdir(benchDir);
27
23
  const markdownFiles = entries.filter((f) => f.endsWith(".md")).sort();
@@ -47,8 +43,4 @@ export async function loadBenchmarks(benchDir) {
47
43
  benchmarks.push({ id, title, description, prompt: content, kind });
48
44
  }
49
45
  return benchmarks;
50
- }
51
-
52
- export function piModelString(profile) {
53
- return profile.harnesses?.pi?.model ?? `${profile.providerId}/${profile.modelAlias}`;
54
46
  }
package/src/benchmark.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // ── Benchmark module (thin facade) ──────────────────────────────────────────
2
2
  // Submodules handle the actual logic. This file re-exports for backward compatibility.
3
3
 
4
- export { slugModelId, createRunId, buildToolPrompt, loadBenchmarks, piModelString } from "./benchmark/shared.mjs";
4
+ export { slugModelId, createRunId, loadBenchmarks } from "./benchmark/shared.mjs";
5
5
  export { findBenchmarkRepo, linkBenchmarkRepo } from "./benchmark/repo.mjs";
6
6
  export { prepareBenchmarkRun } from "./benchmark/prepare.mjs";
7
7
  export { runBenchmarkInPi } from "./benchmark/sdk-runner.mjs";
@@ -149,18 +149,6 @@ function textOnlyProfile(profile) {
149
149
  mmprojPath: null,
150
150
  disabledMmprojPath: profile.disabledMmprojPath ?? profile.mmprojPath,
151
151
  capabilities: { ...(profile.capabilities ?? {}), vision: false, visionDisabledReason: "unsupported-mmproj" },
152
- commandArgv: removeCommandOption(profile.commandArgv ?? [], "--mmproj"),
153
152
  });
154
153
  }
155
154
 
156
- function removeCommandOption(argv, flag) {
157
- const next = [];
158
- for (let i = 0; i < argv.length; i++) {
159
- if (argv[i] === flag) {
160
- if (argv[i + 1] && !argv[i + 1].startsWith("--")) i += 1;
161
- continue;
162
- }
163
- next.push(argv[i]);
164
- }
165
- return next;
166
- }
@@ -118,7 +118,7 @@ function modelInput(profile) {
118
118
  return ["text"];
119
119
  }
120
120
 
121
- function modelCompat(profile) {
121
+ export function modelCompat(profile) {
122
122
  if (profile.compat) return profile.compat;
123
123
  const family = modelFamily(profile);
124
124
  if (family.includes("qwen") || family.includes("gemma-4") || family.includes("gemma 4")) {
@@ -127,14 +127,14 @@ function modelCompat(profile) {
127
127
  return null;
128
128
  }
129
129
 
130
- function modelReasoning(profile) {
130
+ export function modelReasoning(profile) {
131
131
  if (profile.reasoning !== undefined) return Boolean(profile.reasoning);
132
132
  const family = modelFamily(profile);
133
133
  if (family.includes("qwen") || family.includes("gemma-4") || family.includes("gemma 4")) return true;
134
134
  return undefined;
135
135
  }
136
136
 
137
- function modelFamily(profile) {
137
+ export function modelFamily(profile) {
138
138
  return [profile.id, profile.label, profile.modelAlias, profile.modelPath, profile.omlxModel].filter(Boolean).join(" ").toLowerCase();
139
139
  }
140
140
 
@@ -159,21 +159,6 @@ export function modelSelectOption(item, { runningProfilesNow, modelMissingIds, n
159
159
  ...(hint ? { hint: pc.red(hint) } : {}),
160
160
  };
161
161
  }
162
- if (item.type === "new") {
163
- return {
164
- value: itemKey(item),
165
- label: optionLabel({
166
- status: optionStatusTag("setup"),
167
- backend: optionBackendTag(backendId),
168
- source: optionSourceTag(sourceId),
169
- name: item.label,
170
- nameWidth,
171
- quant: optionQuantLabel(item),
172
- ctx: optionCtxLabel(item),
173
- size: optionSizeLabel(item),
174
- }),
175
- };
176
- }
177
162
  return {
178
163
  value: itemKey(item),
179
164
  label: optionLabel({
package/src/process.mjs CHANGED
@@ -3,7 +3,6 @@ import { promisify } from "node:util";
3
3
  import { closeSync, openSync } from "node:fs";
4
4
  import { readFile, writeFile, chmod } from "node:fs/promises";
5
5
  import { basename, join } from "node:path";
6
- import { quoteShell } from "./command.mjs";
7
6
  import { LOG_DIR } from "./config.mjs";
8
7
  import { writeState, readState, profileDir } from "./profiles.mjs";
9
8
  import { backendFor, backendBinaryFor } from "./backends.mjs";
@@ -501,4 +500,9 @@ function timestampForFile() {
501
500
 
502
501
  function sleep(ms) {
503
502
  return new Promise((resolve) => setTimeout(resolve, ms));
503
+ }
504
+
505
+ function quoteShell(value) {
506
+ const text = String(value);
507
+ return /^[A-Za-z0-9_/@%+=:,.-]+$/u.test(text) ? text : `'${text.replace(/'/gu, ` '"'"'`)}'`;
504
508
  }
@@ -9,6 +9,7 @@ import { detectCapabilities } from "./autodetect.mjs";
9
9
  import { matchDrafter } from "./scan.mjs";
10
10
  import { scanGgufModels } from "./scan.mjs";
11
11
  import { estimateMemoryMb } from "./mlx-flags.mjs";
12
+ import { capabilitySummary } from "./model-summary.mjs";
12
13
 
13
14
  const execFileAsync = promisify(execFile);
14
15
 
@@ -70,7 +71,7 @@ export async function configureLocalProfile(prompt, profile) {
70
71
  console.log("");
71
72
  console.log(renderSection("Model setup", renderRows([
72
73
  ["Model", pc.bold(profile.label)],
73
- ["Detected", detectionSummary(caps)],
74
+ ["Detected", capabilitySummary(caps)],
74
75
  ["Context", `${profile.flags.ctxSize.toLocaleString()} tokens`],
75
76
  ["KV cache", `${profile.flags.cacheTypeK}/${profile.flags.cacheTypeV}`],
76
77
  ["Sampling", samplingSummary(profile.flags)],
@@ -151,16 +152,12 @@ export function applyRuntimeFlagOverrides(profile, overrides) {
151
152
 
152
153
  export function applyMtpDefaults(profile) {
153
154
  const flags = { ...profile.flags, port: LLAMA_CPP_MTP_PORT };
154
- const edits = {
155
- values: { "--spec-type": "draft-mtp", "--spec-draft-n-max": 4 },
156
- };
157
- if (profile.drafterPath) edits.values["--spec-draft-model"] = profile.drafterPath;
158
155
  return applyProfileFlags({
159
156
  ...profile,
160
157
  backend: "llama-cpp-mtp",
161
158
  providerId: "llama-cpp-mtp",
162
159
  capabilities: { ...(profile.capabilities ?? {}), mtp: true },
163
- }, flags, edits);
160
+ }, flags);
164
161
  }
165
162
 
166
163
  export function removeMtpDefaults(profile) {
@@ -171,9 +168,7 @@ export function removeMtpDefaults(profile) {
171
168
  providerId: "llama-cpp",
172
169
  drafterPath: null,
173
170
  capabilities: { ...(profile.capabilities ?? {}), mtp: false },
174
- }, flags, {
175
- remove: ["--spec-type", "--spec-draft-n-max", "--spec-draft-model"],
176
- });
171
+ }, flags);
177
172
  }
178
173
 
179
174
  function applyVisionDefaults(profile) {
@@ -201,10 +196,10 @@ function applyThinkingDefaults(profile) {
201
196
  function removeThinkingDefaults(profile) {
202
197
  const flags = { ...profile.flags, ...GENERAL_DEFAULTS };
203
198
  delete flags.chatTemplateKwargs;
204
- return applyProfileFlags(profile, flags, { remove: ["--chat-template-kwargs"] });
199
+ return applyProfileFlags(profile, flags);
205
200
  }
206
201
 
207
- function applyProfileFlags(profile, flags, edits = {}) {
202
+ function applyProfileFlags(profile, flags) {
208
203
  const next = {
209
204
  ...profile,
210
205
  flags,
@@ -214,43 +209,9 @@ function applyProfileFlags(profile, flags, edits = {}) {
214
209
  pi: { ...(profile.harnesses?.pi ?? {}), enabled: true, model: `${profile.providerId ?? profile.backend}/${profile.modelAlias ?? profile.id}` },
215
210
  },
216
211
  };
217
- next.commandArgv = updateArgv(profile.commandArgv ?? [], {
218
- "--host": flags.host,
219
- "--port": flags.port,
220
- "--ctx-size": flags.ctxSize,
221
- "--cache-type-k": flags.cacheTypeK,
222
- "--cache-type-v": flags.cacheTypeV,
223
- "--top-k": flags.topK,
224
- "--presence-penalty": flags.presencePenalty,
225
- "--repeat-penalty": flags.repeatPenalty,
226
- ...(flags.chatTemplateKwargs ? { "--chat-template-kwargs": JSON.stringify(flags.chatTemplateKwargs) } : {}),
227
- }, edits);
228
- return next;
229
- }
230
-
231
- function updateArgv(argv, values, edits = {}) {
232
- let next = [...argv];
233
- for (const flag of edits.remove ?? []) next = removeOption(next, flag);
234
- for (const [flag, value] of Object.entries({ ...values, ...(edits.values ?? {}) })) {
235
- if (value === undefined) continue;
236
- const index = next.indexOf(flag);
237
- if (index === -1) next.push(flag, String(value));
238
- else next[index + 1] = String(value);
239
- }
240
212
  return next;
241
213
  }
242
214
 
243
- function removeOption(argv, flag) {
244
- const next = [];
245
- for (let i = 0; i < argv.length; i++) {
246
- if (argv[i] === flag) {
247
- if (argv[i + 1] && !argv[i + 1].startsWith("--")) i += 1;
248
- continue;
249
- }
250
- next.push(argv[i]);
251
- }
252
- return next;
253
- }
254
215
 
255
216
  function renderMemoryEstimate(profile) {
256
217
  try {
@@ -283,18 +244,6 @@ async function runtimeSupportsGemma4Unified() {
283
244
  }
284
245
  }
285
246
 
286
- function detectionSummary(caps) {
287
- const parts = [];
288
- if (caps.architecture) parts.push(caps.architecture);
289
- if (caps.quant) parts.push(caps.quant);
290
- if (caps.mtp) parts.push("MTP");
291
- if (caps.qat) parts.push("QAT");
292
-
293
- if (caps.thinking) parts.push("thinking");
294
- if (caps.vision) parts.push("vision");
295
- return parts.length > 0 ? parts.join(" · ") : "standard GGUF";
296
- }
297
-
298
247
  function samplingSummary(flags) {
299
248
  return `temp ${flags.temperature}, top-p ${flags.topP}, top-k ${flags.topK}`;
300
249
  }
@@ -342,7 +291,7 @@ export async function configureMlxProfile(prompt, profile) {
342
291
  ["Backend", configured.backend],
343
292
  ["Endpoint", configured.baseUrl],
344
293
  ["Context", String(configured.flags.ctxSize) + " tokens"],
345
- ["Thinking", configured.capabilities.thinking && configured.commandArgv.includes("--enable-thinking") ? "on" : "off"],
294
+ ["Thinking", configured.capabilities?.thinking ? "on" : "off"],
346
295
  ["Vision", configured.capabilities.vision ? "yes" : "no"],
347
296
  ])));
348
297
 
@@ -352,33 +301,19 @@ export async function configureMlxProfile(prompt, profile) {
352
301
 
353
302
  async function applyMlxThinkingToggle(profile, enabled) {
354
303
  if (!profile.capabilities.thinking) return profile;
355
- const { computeMlxVlmFlags } = await import("./mlx-flags.mjs");
356
- const { args } = computeMlxVlmFlags(profile.modelPath, {
357
- port: profile.flags.port,
358
- ctxSize: profile.flags.ctxSize,
359
- thinkingEnabled: enabled,
360
- });
361
304
  return {
362
305
  ...profile,
363
- commandArgv: args,
364
306
  capabilities: { ...profile.capabilities, thinkingEnabled: enabled },
365
307
  };
366
308
  }
367
309
 
368
310
  function applyMlxContextSize(profile, ctxSize) {
369
311
  const flags = { ...profile.flags, ctxSize };
370
- const next = {
312
+ return {
371
313
  ...profile,
372
314
  flags,
373
315
  baseUrl: baseUrlForFlags(flags),
374
316
  };
375
- const idx = next.commandArgv.indexOf("--max-kv-size");
376
- if (idx !== -1 && next.commandArgv[idx + 1] != null) {
377
- next.commandArgv[idx + 1] = String(ctxSize);
378
- } else if (ctxSize && ctxSize > 0) {
379
- next.commandArgv.push("--max-kv-size", String(ctxSize));
380
- }
381
- return next;
382
317
  }
383
318
 
384
319
  function renderMlxMemoryEstimate(profile) {
package/src/profiles.mjs CHANGED
@@ -18,10 +18,6 @@ export function profileJsonPath(id) {
18
18
  return join(profileDir(id), "profile.json");
19
19
  }
20
20
 
21
- export function commandJsonPath(id) {
22
- return join(profileDir(id), "command.json");
23
- }
24
-
25
21
  export function notesPath(id) {
26
22
  return join(profileDir(id), "notes.md");
27
23
  }
@@ -69,9 +65,8 @@ export async function saveProfile(profile, options = {}) {
69
65
  };
70
66
  await writeJson(profileJsonPath(id), saved);
71
67
 
72
- // Note: command.json is no longer writtenthe server command is computed
73
- // fresh from the profile config at launch time (see computeServerCommand in
74
- // process.mjs). commandArgv is kept in the profile for backwards compat.
68
+ // Note: commandArgv is no longer storedcomputeServerCommand computes
69
+ // the full command fresh from profile config at launch time.
75
70
 
76
71
  if (!existsSync(notesPath(id))) {
77
72
  await writeFile(notesPath(id), `# ${saved.label}\n\nNotes for this model profile.\n`, "utf8");
@@ -134,7 +129,7 @@ export async function createProfileFromModel(model, backendId, drafterPath) {
134
129
  // If a drafter is provided, this model supports MTP regardless of filename
135
130
  const hasMtp = caps.mtp || Boolean(drafterPath);
136
131
  const backend = backendId ?? (hasMtp ? "llama-cpp-mtp" : "llama-cpp");
137
- const { flags, argv } = computeFlags(
132
+ const { flags } = computeFlags(
138
133
  { ...caps, mtp: hasMtp },
139
134
  model.path,
140
135
  model.mmprojPath,
@@ -154,21 +149,15 @@ export async function createProfileFromModel(model, backendId, drafterPath) {
154
149
  capabilities: summarizeCapabilities({ ...caps, mtp: hasMtp }),
155
150
  preset: null, // no presets — auto-detected
156
151
  flags,
157
- commandArgv: argv,
158
152
  });
159
153
  }
160
154
 
161
155
  // ── Auto-create profile from a discovered MLX model ────────────────────────
162
156
 
163
157
  export async function createProfileFromMlxModel(model) {
164
- const { computeMlxVlmFlags, DEFAULT_PORT } = await import("./mlx-flags.mjs");
158
+ const { DEFAULT_PORT } = await import("./mlx-flags.mjs");
165
159
  const caps = await detectMlxCapabilities(model.filePath);
166
160
  const ctxSize = defaultMlxContextLength(caps.contextLength, detectHardware().totalRamBytes / (1024 ** 3));
167
- const { args } = computeMlxVlmFlags(model.filePath, {
168
- port: DEFAULT_PORT,
169
- ctxSize,
170
- thinkingEnabled: caps.thinking,
171
- });
172
161
  return normalizeProfile({
173
162
  id: slugFromLabel(model.label),
174
163
  label: model.label,
@@ -182,7 +171,6 @@ export async function createProfileFromMlxModel(model) {
182
171
  modelSizeBytes: model.sizeBytes,
183
172
  capabilities: caps,
184
173
  flags: { host: "127.0.0.1", port: DEFAULT_PORT, ctxSize },
185
- commandArgv: args,
186
174
  });
187
175
  }
188
176
 
@@ -203,11 +191,6 @@ function summarizeCapabilities(caps) {
203
191
 
204
192
  // ── State files (for running servers) ──────────────────────────────────────
205
193
 
206
- export async function readCommandArgv(profile) {
207
- const command = await readJson(commandJsonPath(profile.id), null);
208
- return Array.isArray(command?.argv) ? command.argv.map(String) : (profile.commandArgv ?? []).map(String);
209
- }
210
-
211
194
  export async function readState(id) {
212
195
  return readJson(statePath(id), null);
213
196
  }
package/src/command.mjs DELETED
@@ -1,21 +0,0 @@
1
- export function buildPrettyCommand(profile, binary = "llama-server") {
2
- const argv = profile.commandArgv ?? [];
3
- const lines = [`${quoteShell(binary)} \\`];
4
- for (let i = 0; i < argv.length; i++) {
5
- const arg = argv[i];
6
- const next = argv[i + 1];
7
- const hasValue = arg.startsWith("--") && next && !next.startsWith("--");
8
- if (hasValue) {
9
- lines.push(` ${arg} ${quoteShell(next)}${i + 2 < argv.length ? " \\" : ""}`);
10
- i += 1;
11
- } else {
12
- lines.push(` ${arg}${i + 1 < argv.length ? " \\" : ""}`);
13
- }
14
- }
15
- return lines.join("\n");
16
- }
17
-
18
- export function quoteShell(value) {
19
- const text = String(value);
20
- return /^[A-Za-z0-9_/@%+=:,.-]+$/u.test(text) ? text : `'${text.replace(/'/gu, `'"'"'`)}'`;
21
- }