offgrid-ai 0.6.0 → 0.6.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.6.0",
3
+ "version": "0.6.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/benchmark.mjs CHANGED
@@ -10,7 +10,7 @@ import { pc, createPrompt, renderRows, renderSection } from "./ui.mjs";
10
10
 
11
11
  const execFileAsync = promisify(execFile);
12
12
 
13
- // ── Shared utilities (matches local-llm-visual-benchmark/scripts/local-llm/shared/run-utils.mjs) ──
13
+ // ── Shared utilities (matches local-llm-visual-benchmark) ──────────────────
14
14
 
15
15
  export function slugModelId(modelId, maxLength = 80) {
16
16
  const hash = createHash("sha256").update(modelId).digest("hex").slice(0, 10);
@@ -46,14 +46,12 @@ export async function loadBenchmarks(benchDir) {
46
46
  const benchmarks = [];
47
47
  for (const filename of markdownFiles) {
48
48
  const raw = await readFile(join(benchDir, filename), "utf8");
49
- // Simple frontmatter parser (no gray-matter dependency)
50
49
  const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
51
50
  const frontmatter = match ? match[1] : "";
52
51
  const content = match ? match[2].trim() : raw.trim();
53
52
  let id = filename.replace(/\.md$/u, "");
54
53
  let title = id;
55
54
  let description = "";
56
- let kind = "visual";
57
55
  for (const line of frontmatter.split("\n")) {
58
56
  const kv = line.match(/^(\w+):\s*(.+)$/);
59
57
  if (kv) {
@@ -61,10 +59,9 @@ export async function loadBenchmarks(benchDir) {
61
59
  if (key === "id") id = val.trim();
62
60
  if (key === "title") title = val.trim();
63
61
  if (key === "description") description = val.trim();
64
- if (key === "kind") kind = val.trim();
65
62
  }
66
63
  }
67
- if (id === "ab-test-analysis") kind = "data-science";
64
+ const kind = id === "ab-test-analysis" ? "data-science" : "visual";
68
65
  benchmarks.push({ id, title, description, prompt: content, kind });
69
66
  }
70
67
  return benchmarks;
@@ -86,7 +83,6 @@ export async function linkBenchmarkRepo(prompt) {
86
83
  const existing = await findBenchmarkRepo();
87
84
  if (existing) return existing;
88
85
 
89
- // Check common locations
90
86
  const candidates = [
91
87
  join(homedir(), "dev", "local-llm-visual-benchmark"),
92
88
  join(homedir(), "projects", "local-llm-visual-benchmark"),
@@ -125,7 +121,6 @@ export async function linkBenchmarkRepo(prompt) {
125
121
  }
126
122
  }
127
123
 
128
- // Manual path
129
124
  const path = await prompt.text("Path to local-llm-visual-benchmark", "");
130
125
  if (!path) return null;
131
126
  const resolved = resolve(path.replace(/^~/, homedir()));
@@ -140,7 +135,109 @@ export async function linkBenchmarkRepo(prompt) {
140
135
  return resolved;
141
136
  }
142
137
 
143
- // ── Prepare a benchmark run ────────────────────────────────────────────────
138
+ // ── Create a benchmark run directory ──────────────────────────────────────
139
+
140
+ async function prepareBenchmarkRun({ repoPath, benchmark, kind, modelId, modelSource, backendLabel, profile }) {
141
+ const toolPrompt = buildToolPrompt(benchmark, kind);
142
+ const now = new Date();
143
+ const runId = createRunId(now);
144
+ const modelSlug = slugModelId(modelId);
145
+ const runsDir = join(repoPath, "runs");
146
+ const benchmarkDirectory = join(runsDir, benchmark.id);
147
+ const modelDirectory = join(benchmarkDirectory, modelSlug);
148
+ const runDirectory = join(modelDirectory, runId);
149
+
150
+ await mkdir(runDirectory, { recursive: true });
151
+
152
+ const isDs = kind === "data-science";
153
+ const metadata = {
154
+ schemaVersion: 1,
155
+ kind,
156
+ runId,
157
+ benchmark: { id: benchmark.id, title: benchmark.title, description: benchmark.description, prompt: benchmark.prompt },
158
+ model: { id: modelId, slug: modelSlug },
159
+ status: "prepared",
160
+ createdAt: now.toISOString(),
161
+ updatedAt: now.toISOString(),
162
+ preparedAt: now.toISOString(),
163
+ runDirectory,
164
+ assets: isDs
165
+ ? { metadata: "metadata.json", prompt: "prompt.md", rawResponse: "response.raw.txt", ds: { notebook: "analysis.ipynb", summary: "summary.json", chartDistribution: "chart-distribution.png", chartTreatmentEffect: "chart-treatment-effect.png", chartCompletionRates: "chart-completion-rates.png" } }
166
+ : { metadata: "metadata.json", prompt: "prompt.md", html: "index.html", preview: "preview.png", video: "preview.webm", rawResponse: "response.raw.txt" },
167
+ runner: {
168
+ mode: modelSource === "cloud" ? "manual" : "external",
169
+ intendedRunner: profile ? "Pi" : undefined,
170
+ ...(profile ? { tool: "pi" } : {}),
171
+ ...(modelSource ? { modelSource } : {}),
172
+ ...(backendLabel ? { backendLabel } : {}),
173
+ ...(profile?.baseUrl ? { baseUrl: profile.baseUrl } : {}),
174
+ model: modelId,
175
+ retries: 0,
176
+ tokenMetrics: { reported: false },
177
+ },
178
+ };
179
+
180
+ await writeFile(join(runDirectory, "metadata.json"), JSON.stringify(metadata, null, 2) + "\n", "utf8");
181
+ await writeFile(join(runDirectory, "prompt.md"), toolPrompt + "\n", "utf8");
182
+
183
+ console.log("");
184
+ console.log(pc.green("✓ Run slot prepared"));
185
+ console.log(renderSection("Run", renderRows([
186
+ ["Directory", pc.cyan(runDirectory)],
187
+ ["Benchmark", benchmark.title],
188
+ ["Kind", kind],
189
+ ["Model", pc.bold(modelId)],
190
+ ["Source", backendLabel || modelSource],
191
+ ])));
192
+
193
+ console.log("");
194
+ console.log(pc.bold("Next steps"));
195
+ console.log(` 1. ${pc.cyan(`offgrid-ai run ${profile ? profile.id : "<profile>"}`)}`);
196
+ console.log(` 2. ${pc.cyan(`cd ${repoPath} && npm run dev`)}`);
197
+ console.log(` 3. In the gallery, find your run and copy the prompt from the run details`);
198
+ console.log(" 4. In Pi, paste the prompt");
199
+
200
+ return runDirectory;
201
+ }
202
+
203
+ // ── Benchmark from a selected profile (from model picker) ────────────────
204
+
205
+ export async function benchmarkForProfile(profile) {
206
+ await ensureDirs();
207
+ const prompt = createPrompt();
208
+ try {
209
+ const repoPath = await linkBenchmarkRepo(prompt);
210
+ if (!repoPath) return;
211
+
212
+ const kind = await prompt.choice("Benchmark category", [
213
+ { value: "visual", label: "Visual Benchmark", hint: "HTML/CSS/JS animation benchmarks" },
214
+ { value: "data-science", label: "Data Science", hint: "Analysis and charting benchmarks" },
215
+ ], "visual");
216
+
217
+ const benchDir = join(repoPath, "benchmarks");
218
+ const benchmarks = (await loadBenchmarks(benchDir)).filter((b) => b.kind === kind);
219
+ if (benchmarks.length === 0) {
220
+ console.log(pc.yellow(`No ${kind} benchmarks found in ${benchDir}`));
221
+ return;
222
+ }
223
+ const benchmarkId = await prompt.choice("Prompt", benchmarks.map((b) => ({
224
+ value: b.id, label: b.title, hint: b.description || b.id,
225
+ })), benchmarks[0].id);
226
+ const selectedBenchmark = benchmarks.find((b) => b.id === benchmarkId);
227
+ if (!selectedBenchmark) return;
228
+
229
+ const { backendFor } = await import("./backends.mjs");
230
+ const modelId = profile.modelAlias;
231
+ const modelSource = profile.providerId === "llama-cpp-mtp" ? "llama-cpp-mtp" : profile.backend === "ollama" ? "ollama" : profile.backend === "omlx" ? "omlx" : "llama-cpp";
232
+ const backendLabel = backendFor(profile.backend).label;
233
+
234
+ return await prepareBenchmarkRun({ repoPath, benchmark: selectedBenchmark, kind, modelId, modelSource, backendLabel, profile });
235
+ } finally {
236
+ prompt.close();
237
+ }
238
+ }
239
+
240
+ // ── Standalone benchmark flow (offgrid-ai benchmark) ──────────────────────
144
241
 
145
242
  export async function benchmarkFlow() {
146
243
  await ensureDirs();
@@ -150,13 +247,11 @@ export async function benchmarkFlow() {
150
247
  const repoPath = await linkBenchmarkRepo(prompt);
151
248
  if (!repoPath) return;
152
249
 
153
- // 1. Pick category
154
250
  const kind = await prompt.choice("Benchmark category", [
155
251
  { value: "visual", label: "Visual Benchmark", hint: "HTML/CSS/JS animation benchmarks" },
156
252
  { value: "data-science", label: "Data Science", hint: "Analysis and charting benchmarks" },
157
253
  ], "visual");
158
254
 
159
- // 2. Pick benchmark prompt
160
255
  const benchDir = join(repoPath, "benchmarks");
161
256
  const benchmarks = (await loadBenchmarks(benchDir)).filter((b) => b.kind === kind);
162
257
  if (benchmarks.length === 0) {
@@ -164,14 +259,11 @@ export async function benchmarkFlow() {
164
259
  return;
165
260
  }
166
261
  const benchmarkId = await prompt.choice("Prompt", benchmarks.map((b) => ({
167
- value: b.id,
168
- label: b.title,
169
- hint: b.description || b.id,
262
+ value: b.id, label: b.title, hint: b.description || b.id,
170
263
  })), benchmarks[0].id);
171
264
  const selectedBenchmark = benchmarks.find((b) => b.id === benchmarkId);
172
265
  if (!selectedBenchmark) return;
173
266
 
174
- // 3. Pick model source
175
267
  const { loadProfiles } = await import("./profiles.mjs");
176
268
  const { backendFor } = await import("./backends.mjs");
177
269
 
@@ -181,7 +273,7 @@ export async function benchmarkFlow() {
181
273
  { value: "cloud", label: "Custom / cloud", hint: "Free-form model label for cloud runs" },
182
274
  ], "profile");
183
275
 
184
- let modelId, modelSource, backendLabel;
276
+ let modelId, modelSource, backendLabel, profile;
185
277
 
186
278
  if (source === "profile") {
187
279
  if (profiles.length === 0) {
@@ -189,11 +281,9 @@ export async function benchmarkFlow() {
189
281
  return;
190
282
  }
191
283
  const profileId = await prompt.choice("Profile", profiles.map((p) => ({
192
- value: p.id,
193
- label: p.label,
194
- hint: `${backendFor(p.backend).label} · ${p.modelAlias}`,
284
+ value: p.id, label: p.label, hint: `${backendFor(p.backend).label} · ${p.modelAlias}`,
195
285
  })), profiles[0].id);
196
- const profile = profiles.find((p) => p.id === profileId);
286
+ profile = profiles.find((p) => p.id === profileId);
197
287
  if (!profile) return;
198
288
  modelId = profile.modelAlias;
199
289
  modelSource = profile.providerId === "llama-cpp-mtp" ? "llama-cpp-mtp" : profile.backend === "ollama" ? "ollama" : profile.backend === "omlx" ? "omlx" : "llama-cpp";
@@ -205,72 +295,7 @@ export async function benchmarkFlow() {
205
295
  modelSource = "cloud";
206
296
  }
207
297
 
208
- // 4. Create the run directory
209
- const toolPrompt = buildToolPrompt(selectedBenchmark, kind);
210
- const now = new Date();
211
- const runId = createRunId(now);
212
- const modelSlug = slugModelId(modelId);
213
- const runsDir = join(repoPath, "runs");
214
- const benchmarkDirectory = join(runsDir, selectedBenchmark.id);
215
- const modelDirectory = join(benchmarkDirectory, modelSlug);
216
- const runDirectory = join(modelDirectory, runId);
217
-
218
- await mkdir(runDirectory, { recursive: true });
219
-
220
- const isDs = kind === "data-science";
221
- const metadata = {
222
- schemaVersion: 1,
223
- kind,
224
- runId,
225
- benchmark: { id: selectedBenchmark.id, title: selectedBenchmark.title, description: selectedBenchmark.description, prompt: selectedBenchmark.prompt },
226
- model: { id: modelId, slug: modelSlug },
227
- status: "prepared",
228
- createdAt: now.toISOString(),
229
- updatedAt: now.toISOString(),
230
- preparedAt: now.toISOString(),
231
- runDirectory,
232
- assets: isDs
233
- ? { metadata: "metadata.json", prompt: "prompt.md", rawResponse: "response.raw.txt", ds: { notebook: "analysis.ipynb", summary: "summary.json", chartDistribution: "chart-distribution.png", chartTreatmentEffect: "chart-treatment-effect.png", chartCompletionRates: "chart-completion-rates.png" } }
234
- : { metadata: "metadata.json", prompt: "prompt.md", html: "index.html", preview: "preview.png", video: "preview.webm", rawResponse: "response.raw.txt" },
235
- runner: {
236
- mode: source === "profile" ? "external" : "manual",
237
- ...(modelSource ? { modelSource } : {}),
238
- ...(backendLabel ? { backendLabel } : {}),
239
- model: modelId,
240
- retries: 0,
241
- tokenMetrics: { reported: false },
242
- },
243
- };
244
-
245
- // Clean up the baseUrl field — only include if we have a real one
246
- if (!metadata.runner.baseUrl) delete metadata.runner.baseUrl;
247
-
248
- await writeFile(join(runDirectory, "metadata.json"), JSON.stringify(metadata, null, 2) + "\n", "utf8");
249
- await writeFile(join(runDirectory, "prompt.md"), toolPrompt + "\n", "utf8");
250
-
251
- // 5. Print result
252
- console.log("");
253
- console.log(pc.green("✓ Run slot prepared"));
254
- console.log(renderSection("Run", renderRows([
255
- ["Directory", pc.cyan(runDirectory)],
256
- ["Benchmark", selectedBenchmark.title],
257
- ["Kind", kind],
258
- ["Model", pc.bold(modelId)],
259
- ["Source", backendLabel || modelSource],
260
- ])));
261
-
262
- console.log("");
263
- console.log(pc.bold("Next steps"));
264
- if (source === "profile") {
265
- console.log(` 1. ${pc.cyan(`cd ${runDirectory}`)}`);
266
- console.log(` 2. ${pc.cyan("offgrid-ai models")} → select the model → Run`);
267
- console.log(` 3. In Pi, paste the prompt from ${pc.cyan("prompt.md")}`);
268
- console.log(` 4. View results: ${pc.cyan(`cd ${repoPath} && npm run dev`)}`);
269
- } else {
270
- console.log(` 1. ${pc.cyan(`cd ${runDirectory}`)}`);
271
- console.log(` 2. Copy the prompt from ${pc.cyan("prompt.md")} into your tool of choice`);
272
- console.log(` 3. View results: ${pc.cyan(`cd ${repoPath} && npm run dev`)}`);
273
- }
298
+ return await prepareBenchmarkRun({ repoPath, benchmark: selectedBenchmark, kind, modelId, modelSource, backendLabel, profile });
274
299
  } finally {
275
300
  prompt.close();
276
301
  }
package/src/cli.mjs CHANGED
@@ -419,6 +419,9 @@ async function performAction(prompt, action, item) {
419
419
  return printGgufModelDetails(item.model, item.drafter);
420
420
  }
421
421
  if (action === "benchmark") {
422
+ const { benchmarkForProfile } = await import("./benchmark.mjs");
423
+ if (item.type === "profile") return await benchmarkForProfile(await readProfile(item.profile.id));
424
+ // For new/managed models, go through the full flow
422
425
  const { benchmarkFlow } = await import("./benchmark.mjs");
423
426
  return await benchmarkFlow();
424
427
  }