offgrid-ai 0.13.0 → 0.14.1

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.
@@ -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
  }
@@ -1,257 +0,0 @@
1
- // ── Run benchmark in Pi (non-interactive JSON mode) ───────────────────────────
2
-
3
- import { writeFile } from "node:fs/promises";
4
- import { join } from "node:path";
5
- import { spawn } from "node:child_process";
6
- import {
7
- BENCH_COLORS, renderStreamEvent,
8
- formatToolCall, printFinalLine, stopExecTimer,
9
- } from "./stream-renderer.mjs";
10
- import { piModelString } from "./shared.mjs";
11
-
12
- export async function runBenchmarkInPi(profile, runDirectory, { signal } = {}) {
13
- const model = piModelString(profile);
14
- const args = ["--model", model, "--mode", "json", "-p", "@prompt.md"];
15
-
16
- const child = spawn("pi", args, {
17
- cwd: runDirectory,
18
- stdio: ["ignore", "pipe", "pipe"],
19
- });
20
-
21
- const runResult = {
22
- model,
23
- exitCode: null,
24
- wallClockMs: null,
25
- agentTurns: 0,
26
- promptTokens: 0,
27
- completionTokens: 0,
28
- totalTokens: 0,
29
- cacheRead: 0,
30
- cacheWrite: 0,
31
- toolCalls: 0,
32
- toolResults: 0,
33
- perTurn: [],
34
- rawResponseLines: [],
35
- error: null,
36
- };
37
-
38
- let streamBuffer = "";
39
- let responseBuffer = "";
40
- let currentTurnStartMs = null;
41
- let lastTurnEndMs = null;
42
- let runStartMs = null;
43
- let firstEventMs = null;
44
- let lastEventMs = null;
45
- let cancelled = false;
46
-
47
- const streamPath = join(runDirectory, "stream.ndjson");
48
- const stderrPath = join(runDirectory, "stderr.log");
49
- const responsePath = join(runDirectory, "response.raw.txt");
50
-
51
- const streamHandle = await openFileHandle(streamPath, "w");
52
- const stderrHandle = await openFileHandle(stderrPath, "w");
53
-
54
- const verbose = Boolean(process.env.OFFGRID_BENCHMARK_VERBOSE);
55
- const renderState = {
56
- cwd: runDirectory,
57
- turn: 0,
58
- turnHadToolError: false,
59
- modelPrinted: false,
60
- activeTool: null,
61
- execTimer: null,
62
- status: { mode: "idle", toolName: null, bytes: 0, tokens: 0, execStartedAt: null },
63
- };
64
-
65
- function appendResponse(text) {
66
- responseBuffer += text;
67
- }
68
-
69
- function flushResponse() {
70
- if (responseBuffer) {
71
- runResult.rawResponseLines.push(responseBuffer);
72
- responseBuffer = "";
73
- }
74
- }
75
-
76
- function updateTimeBounds(timestamp) {
77
- if (!timestamp) return;
78
- if (firstEventMs === null) firstEventMs = timestamp;
79
- lastEventMs = timestamp;
80
- }
81
-
82
- function beginTurn() {
83
- runResult.agentTurns += 1;
84
- currentTurnStartMs = lastTurnEndMs ?? runStartMs ?? null;
85
- }
86
-
87
- function endTurn(usage, timestamp) {
88
- const turnEndMs = timestamp ?? null;
89
- const wallClockMs = currentTurnStartMs && turnEndMs ? turnEndMs - currentTurnStartMs : null;
90
- runResult.perTurn.push({
91
- turn: runResult.agentTurns,
92
- inputTokens: usage?.input ?? 0,
93
- outputTokens: usage?.output ?? 0,
94
- cacheRead: usage?.cacheRead ?? 0,
95
- cacheWrite: usage?.cacheWrite ?? 0,
96
- wallClockMs,
97
- toolCalls: 0,
98
- });
99
- if (turnEndMs) lastTurnEndMs = turnEndMs;
100
- currentTurnStartMs = null;
101
- }
102
-
103
- function processLine(line) {
104
- if (!line.trim()) return;
105
- streamHandle.write(line + "\n");
106
- let parsed;
107
- try {
108
- parsed = JSON.parse(line);
109
- } catch (err) {
110
- console.log(BENCH_COLORS.error(`[parse error] ${err.message}`));
111
- return;
112
- }
113
-
114
- const timestamp = extractTimestamp(parsed);
115
- updateTimeBounds(timestamp);
116
-
117
- renderStreamEvent(parsed, renderState, { verbose });
118
-
119
- if (parsed.type === "session" || parsed.type === "agent_start") {
120
- if (timestamp && runStartMs === null) runStartMs = timestamp;
121
- }
122
-
123
- if (parsed.type === "turn_start") {
124
- beginTurn();
125
- }
126
-
127
- if (parsed.type === "turn_end" && parsed.message?.usage) {
128
- const usage = parsed.message.usage;
129
- runResult.promptTokens += usage.input ?? 0;
130
- runResult.completionTokens += usage.output ?? 0;
131
- runResult.cacheRead += usage.cacheRead ?? 0;
132
- runResult.cacheWrite += usage.cacheWrite ?? 0;
133
- endTurn(usage, timestamp);
134
- }
135
-
136
- if (parsed.type === "message_update" && parsed.assistantMessageEvent) {
137
- const evt = parsed.assistantMessageEvent;
138
- const subtype = String(evt.type ?? "").replace(/_/gu, "");
139
- if (subtype === "thinkingdelta" || subtype === "textdelta") {
140
- appendResponse(evt.delta || "");
141
- }
142
- }
143
-
144
- if (parsed.type === "message_end" && parsed.message?.role === "assistant") {
145
- flushResponse();
146
- const content = parsed.message.content ?? [];
147
- for (const item of content) {
148
- if (item.type === "toolCall") {
149
- runResult.toolCalls += 1;
150
- appendResponse(`\n${formatToolCall(item)}\n`);
151
- const currentTurn = runResult.perTurn[runResult.perTurn.length - 1];
152
- if (currentTurn) currentTurn.toolCalls += 1;
153
- }
154
- }
155
- }
156
-
157
- if (parsed.type === "toolResult") {
158
- runResult.toolResults += 1;
159
- const status = parsed.isError ? "error" : "ok";
160
- appendResponse(`\n[toolResult] ${parsed.toolName} (${status})\n`);
161
- }
162
-
163
- if (parsed.type === "agent_end") {
164
- flushResponse();
165
- }
166
- }
167
-
168
- child.stdout.setEncoding("utf8");
169
- child.stdout.on("data", (chunk) => {
170
- streamBuffer += chunk;
171
- const lines = streamBuffer.split("\n");
172
- streamBuffer = lines.pop();
173
- for (const line of lines) {
174
- processLine(line);
175
- }
176
- });
177
-
178
- child.stderr.setEncoding("utf8");
179
- child.stderr.on("data", (chunk) => {
180
- stderrHandle.write(chunk);
181
- });
182
-
183
- const abortListener = () => {
184
- if (cancelled) return;
185
- cancelled = true;
186
- console.log(BENCH_COLORS.error("\n\n[Cancelled by user]"));
187
- child.kill("SIGTERM");
188
- };
189
-
190
- if (signal) {
191
- signal.addEventListener("abort", abortListener);
192
- }
193
-
194
- return new Promise((resolve) => {
195
- child.on("exit", async (code) => {
196
- if (signal) signal.removeEventListener("abort", abortListener);
197
- stopExecTimer(renderState);
198
- if (streamBuffer.trim()) {
199
- processLine(streamBuffer);
200
- }
201
- flushResponse();
202
- await streamHandle.close();
203
- await stderrHandle.close();
204
- await writeFile(responsePath, runResult.rawResponseLines.join(""), "utf8");
205
-
206
- runResult.exitCode = code ?? 0;
207
- if (firstEventMs !== null && lastEventMs !== null) {
208
- runResult.wallClockMs = lastEventMs - firstEventMs;
209
- }
210
-
211
- if (cancelled) {
212
- runResult.error = { message: "Cancelled by user" };
213
- resolve(runResult);
214
- return;
215
- }
216
-
217
- printFinalLine(BENCH_COLORS.info("Pi benchmark finished"));
218
-
219
- if (runResult.exitCode !== 0) {
220
- runResult.error = { message: `Pi exited with code ${runResult.exitCode}` };
221
- resolve(runResult);
222
- return;
223
- }
224
-
225
- resolve(runResult);
226
- });
227
-
228
- child.on("error", async (err) => {
229
- if (signal) signal.removeEventListener("abort", abortListener);
230
- stopExecTimer(renderState);
231
- await streamHandle.close();
232
- await stderrHandle.close();
233
- runResult.error = { message: err.message };
234
- resolve(runResult);
235
- });
236
- });
237
- }
238
-
239
- function extractTimestamp(event) {
240
- const raw = event?.message?.timestamp ?? event?.timestamp ?? event?.assistantMessageEvent?.partial?.timestamp;
241
- if (typeof raw === "number") return raw;
242
- if (typeof raw === "string") {
243
- const parsed = Date.parse(raw);
244
- if (Number.isFinite(parsed)) return parsed;
245
- }
246
- const iso = event?.message?.createdAt ?? event?.createdAt ?? event?.created_at;
247
- if (typeof iso === "string") {
248
- const parsed = Date.parse(iso);
249
- if (Number.isFinite(parsed)) return parsed;
250
- }
251
- return null;
252
- }
253
-
254
- async function openFileHandle(path, flags) {
255
- const { open } = await import("node:fs/promises");
256
- return open(path, flags);
257
- }