offgrid-ai 0.6.1 → 0.6.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.
- package/package.json +1 -1
- package/src/benchmark.mjs +50 -14
- package/src/cli.mjs +67 -16
package/package.json
CHANGED
package/src/benchmark.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { ensureDirs, loadConfig, saveConfig } from "./config.mjs";
|
|
9
|
+
import { backendFor } from "./backends.mjs";
|
|
10
|
+
import { serverMatchesProfile, serverReady } from "./process.mjs";
|
|
9
11
|
import { pc, createPrompt, renderRows, renderSection } from "./ui.mjs";
|
|
10
12
|
|
|
11
13
|
const execFileAsync = promisify(execFile);
|
|
@@ -137,11 +139,56 @@ export async function linkBenchmarkRepo(prompt) {
|
|
|
137
139
|
|
|
138
140
|
// ── Create a benchmark run directory ──────────────────────────────────────
|
|
139
141
|
|
|
142
|
+
function harnessDisplayName(id) {
|
|
143
|
+
if (id === "pi") return "Pi";
|
|
144
|
+
return String(id).replace(/[-_]+/gu, " ").replace(/\b\w/gu, (char) => char.toUpperCase());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function intendedRunnerForProfile(profile) {
|
|
148
|
+
if (!profile) return "your tool";
|
|
149
|
+
const harnessEntries = Object.entries(profile.harnesses ?? {}).filter(([, config]) => config?.enabled !== false);
|
|
150
|
+
const [id] = harnessEntries.find(([key]) => key === "pi") ?? harnessEntries[0] ?? ["pi"];
|
|
151
|
+
return harnessDisplayName(id);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function profileServerStatus(profile) {
|
|
155
|
+
if (!profile) return null;
|
|
156
|
+
const ready = await serverReady(profile.baseUrl).catch(() => false);
|
|
157
|
+
if (!ready) return { running: false };
|
|
158
|
+
const match = await serverMatchesProfile(profile).catch(() => ({ matches: true, reason: "server responded" }));
|
|
159
|
+
return { running: match.matches, reason: match.reason };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function printBenchmarkNextSteps({ repoPath, profile, modelId, runnerLabel, serverStatus }) {
|
|
163
|
+
console.log("");
|
|
164
|
+
console.log(pc.bold("Next steps"));
|
|
165
|
+
|
|
166
|
+
let step = 1;
|
|
167
|
+
if (profile) {
|
|
168
|
+
const command = `offgrid-ai run ${profile.id}`;
|
|
169
|
+
if (serverStatus?.running) {
|
|
170
|
+
const reason = serverStatus.reason ? pc.dim(` (${serverStatus.reason})`) : "";
|
|
171
|
+
console.log(` ${step++}. Model server is already running at ${pc.cyan(profile.baseUrl)}${reason}`);
|
|
172
|
+
console.log(` ${pc.dim(`If you still need to open ${runnerLabel}, run: ${command}`)}`);
|
|
173
|
+
} else {
|
|
174
|
+
console.log(` ${step++}. If the model server is not already running, run: ${pc.cyan(command)}`);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
console.log(` ${step++}. Open ${runnerLabel} for ${pc.bold(modelId)}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(` ${step++}. ${pc.cyan(`cd ${repoPath}`)}`);
|
|
181
|
+
console.log(` ${step++}. ${pc.cyan("npm run dev")}`);
|
|
182
|
+
console.log(` ${step}. In the gallery, find this run, copy the prompt, and paste it into ${runnerLabel}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
140
185
|
async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSource, backendLabel, profile }) {
|
|
141
186
|
const toolPrompt = buildToolPrompt(benchmark, kind);
|
|
142
187
|
const now = new Date();
|
|
143
188
|
const runId = createRunId(now);
|
|
144
189
|
const modelSlug = slugModelId(modelId);
|
|
190
|
+
const runnerLabel = intendedRunnerForProfile(profile);
|
|
191
|
+
const serverStatus = await profileServerStatus(profile);
|
|
145
192
|
const runsDir = join(repoPath, "runs");
|
|
146
193
|
const benchmarkDirectory = join(runsDir, benchmark.id);
|
|
147
194
|
const modelDirectory = join(benchmarkDirectory, modelSlug);
|
|
@@ -166,6 +213,8 @@ async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSo
|
|
|
166
213
|
: { metadata: "metadata.json", prompt: "prompt.md", html: "index.html", preview: "preview.png", video: "preview.webm", rawResponse: "response.raw.txt" },
|
|
167
214
|
runner: {
|
|
168
215
|
mode: modelSource === "cloud" ? "manual" : "external",
|
|
216
|
+
intendedRunner: profile ? runnerLabel : undefined,
|
|
217
|
+
...(profile?.harnesses?.pi || runnerLabel === "Pi" ? { tool: "pi" } : {}),
|
|
169
218
|
...(modelSource ? { modelSource } : {}),
|
|
170
219
|
...(backendLabel ? { backendLabel } : {}),
|
|
171
220
|
...(profile?.baseUrl ? { baseUrl: profile.baseUrl } : {}),
|
|
@@ -188,18 +237,7 @@ async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSo
|
|
|
188
237
|
["Source", backendLabel || modelSource],
|
|
189
238
|
])));
|
|
190
239
|
|
|
191
|
-
|
|
192
|
-
console.log(pc.bold("Next steps"));
|
|
193
|
-
if (profile) {
|
|
194
|
-
console.log(` 1. ${pc.cyan(`cd ${runDirectory}`)}`);
|
|
195
|
-
console.log(` 2. ${pc.cyan(`offgrid-ai run ${profile.id}`)}`);
|
|
196
|
-
console.log(` 3. In Pi, paste the prompt from ${pc.cyan("prompt.md")}`);
|
|
197
|
-
console.log(` 4. View results: ${pc.cyan(`cd ${repoPath} && npm run dev`)}`);
|
|
198
|
-
} else {
|
|
199
|
-
console.log(` 1. ${pc.cyan(`cd ${runDirectory}`)}`);
|
|
200
|
-
console.log(` 2. Copy the prompt from ${pc.cyan("prompt.md")} into your tool of choice`);
|
|
201
|
-
console.log(` 3. View results: ${pc.cyan(`cd ${repoPath} && npm run dev`)}`);
|
|
202
|
-
}
|
|
240
|
+
printBenchmarkNextSteps({ repoPath, profile, modelId, runnerLabel, serverStatus });
|
|
203
241
|
|
|
204
242
|
return runDirectory;
|
|
205
243
|
}
|
|
@@ -230,7 +268,6 @@ export async function benchmarkForProfile(profile) {
|
|
|
230
268
|
const selectedBenchmark = benchmarks.find((b) => b.id === benchmarkId);
|
|
231
269
|
if (!selectedBenchmark) return;
|
|
232
270
|
|
|
233
|
-
const { backendFor } = await import("./backends.mjs");
|
|
234
271
|
const modelId = profile.modelAlias;
|
|
235
272
|
const modelSource = profile.providerId === "llama-cpp-mtp" ? "llama-cpp-mtp" : profile.backend === "ollama" ? "ollama" : profile.backend === "omlx" ? "omlx" : "llama-cpp";
|
|
236
273
|
const backendLabel = backendFor(profile.backend).label;
|
|
@@ -269,7 +306,6 @@ export async function benchmarkFlow() {
|
|
|
269
306
|
if (!selectedBenchmark) return;
|
|
270
307
|
|
|
271
308
|
const { loadProfiles } = await import("./profiles.mjs");
|
|
272
|
-
const { backendFor } = await import("./backends.mjs");
|
|
273
309
|
|
|
274
310
|
const profiles = await loadProfiles();
|
|
275
311
|
const source = await prompt.choice("Model source", [
|
package/src/cli.mjs
CHANGED
|
@@ -272,37 +272,88 @@ function buildCatalogItems(normalized) {
|
|
|
272
272
|
];
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
|
|
275
|
+
const OPTION_SEPARATOR = pc.dim(" │ ");
|
|
276
|
+
const OPTION_STATUS_WIDTH = 12;
|
|
277
|
+
const OPTION_SOURCE_WIDTH = 14;
|
|
278
|
+
|
|
279
|
+
function optionTag(text, color, width) {
|
|
280
|
+
const padded = String(text).padEnd(width);
|
|
281
|
+
return color ? color(padded) : padded;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function optionStatusTag(kind) {
|
|
285
|
+
const statuses = {
|
|
286
|
+
running: ["RUNNING", pc.green],
|
|
287
|
+
ready: ["SET UP", pc.green],
|
|
288
|
+
missing: ["FILE MISSING", pc.red],
|
|
289
|
+
setup: ["NEEDS SETUP", pc.yellow],
|
|
290
|
+
};
|
|
291
|
+
const [text, color] = statuses[kind] ?? [kind, pc.dim];
|
|
292
|
+
return optionTag(text, color, OPTION_STATUS_WIDTH);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function optionSourceTag(sourceId, label) {
|
|
296
|
+
const colors = {
|
|
297
|
+
"llama-cpp": pc.cyan,
|
|
298
|
+
"llama-cpp-mtp": pc.blue,
|
|
299
|
+
ollama: pc.green,
|
|
300
|
+
omlx: pc.magenta,
|
|
301
|
+
gguf: pc.cyan,
|
|
302
|
+
};
|
|
303
|
+
return optionTag(label, colors[sourceId] ?? pc.dim, OPTION_SOURCE_WIDTH);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function optionLabel({ status, source, name, details = [] }) {
|
|
307
|
+
return [status, source, pc.bold(name), ...details].filter(Boolean).join(OPTION_SEPARATOR);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function optionHint(parts) {
|
|
311
|
+
return parts.filter(Boolean).join(" · ");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function modelSelectOption(item, { runningProfilesNow, drafters }) {
|
|
276
315
|
if (item.type === "profile") {
|
|
277
316
|
const { profile } = item;
|
|
317
|
+
const backend = backendFor(profile.backend);
|
|
278
318
|
const running = runningProfilesNow.some((r) => r.id === profile.id);
|
|
279
319
|
const fileMissing = item.fileMissing;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
else status = pc.dim("● ready");
|
|
284
|
-
// MTP label: enabled vs available vs not available
|
|
320
|
+
const status = optionStatusTag(fileMissing ? "missing" : running ? "running" : "ready");
|
|
321
|
+
const source = optionSourceTag(profile.backend, backend.label);
|
|
322
|
+
const caps = profile.capabilities ?? {};
|
|
285
323
|
let mtpLabel;
|
|
286
|
-
if (profile.drafterPath) mtpLabel = pc.green("MTP");
|
|
287
|
-
else if ((profile.
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
324
|
+
if (profile.drafterPath) mtpLabel = pc.green("MTP on");
|
|
325
|
+
else if (drafters ? matchDrafter(profile.modelPath, drafters) : null) mtpLabel = pc.yellow("MTP available");
|
|
326
|
+
else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP needs drafter");
|
|
327
|
+
const ctxLabel = profile.flags?.ctxSize ? pc.dim(`${(profile.flags.ctxSize / 1000).toFixed(0)}k ctx`) : null;
|
|
328
|
+
const label = optionLabel({ status, source, name: profile.label, details: [ctxLabel, mtpLabel] });
|
|
329
|
+
return { value: itemKey(item), label, hint: optionHint([profile.modelAlias, humanCapabilitySummary(caps), profile.baseUrl]) };
|
|
291
330
|
}
|
|
292
331
|
if (item.type === "new") {
|
|
293
332
|
const { model, drafter } = item;
|
|
294
333
|
const caps = detectCapabilities(model.path, model.mmprojPath);
|
|
295
334
|
const mtpAvailable = caps.mtp || Boolean(drafter);
|
|
296
335
|
let mtpLabel;
|
|
297
|
-
if (mtpAvailable) mtpLabel = pc.green("MTP
|
|
298
|
-
else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP");
|
|
299
|
-
const label =
|
|
300
|
-
|
|
336
|
+
if (mtpAvailable) mtpLabel = pc.green("MTP available");
|
|
337
|
+
else if (caps.architecture === "gemma4") mtpLabel = pc.yellow("MTP needs drafter");
|
|
338
|
+
const label = optionLabel({
|
|
339
|
+
status: optionStatusTag("setup"),
|
|
340
|
+
source: optionSourceTag("gguf", "GGUF file"),
|
|
341
|
+
name: model.label,
|
|
342
|
+
details: [pc.dim(formatBytes(model.sizeBytes)), mtpLabel],
|
|
343
|
+
});
|
|
344
|
+
return { value: itemKey(item), label, hint: optionHint([basename(model.path), humanCapabilitySummary(caps), model.quant]) };
|
|
301
345
|
}
|
|
302
346
|
// managed
|
|
303
347
|
const { model, backendId } = item;
|
|
304
348
|
const backend = BACKENDS[backendId];
|
|
305
|
-
|
|
349
|
+
const details = [model.quant ? pc.dim(model.quant) : null, model.sizeBytes ? pc.dim(formatBytes(model.sizeBytes)) : null];
|
|
350
|
+
const label = optionLabel({
|
|
351
|
+
status: optionStatusTag("setup"),
|
|
352
|
+
source: optionSourceTag(backendId, backend.label),
|
|
353
|
+
name: model.label,
|
|
354
|
+
details,
|
|
355
|
+
});
|
|
356
|
+
return { value: itemKey(item), label, hint: optionHint([model.id, "available through local backend"]) };
|
|
306
357
|
}
|
|
307
358
|
|
|
308
359
|
function actionsForItem(item) {
|