offgrid-ai 0.6.0 → 0.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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,113 @@ 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
+ ...(modelSource ? { modelSource } : {}),
170
+ ...(backendLabel ? { backendLabel } : {}),
171
+ ...(profile?.baseUrl ? { baseUrl: profile.baseUrl } : {}),
172
+ model: modelId,
173
+ retries: 0,
174
+ tokenMetrics: { reported: false },
175
+ },
176
+ };
177
+
178
+ await writeFile(join(runDirectory, "metadata.json"), JSON.stringify(metadata, null, 2) + "\n", "utf8");
179
+ await writeFile(join(runDirectory, "prompt.md"), toolPrompt + "\n", "utf8");
180
+
181
+ console.log("");
182
+ console.log(pc.green("✓ Run slot prepared"));
183
+ console.log(renderSection("Run", renderRows([
184
+ ["Directory", pc.cyan(runDirectory)],
185
+ ["Benchmark", benchmark.title],
186
+ ["Kind", kind],
187
+ ["Model", pc.bold(modelId)],
188
+ ["Source", backendLabel || modelSource],
189
+ ])));
190
+
191
+ console.log("");
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
+ }
203
+
204
+ return runDirectory;
205
+ }
206
+
207
+ // ── Benchmark from a selected profile (from model picker) ────────────────
208
+
209
+ export async function benchmarkForProfile(profile) {
210
+ await ensureDirs();
211
+ const prompt = createPrompt();
212
+ try {
213
+ const repoPath = await linkBenchmarkRepo(prompt);
214
+ if (!repoPath) return;
215
+
216
+ const kind = await prompt.choice("Benchmark category", [
217
+ { value: "visual", label: "Visual Benchmark", hint: "HTML/CSS/JS animation benchmarks" },
218
+ { value: "data-science", label: "Data Science", hint: "Analysis and charting benchmarks" },
219
+ ], "visual");
220
+
221
+ const benchDir = join(repoPath, "benchmarks");
222
+ const benchmarks = (await loadBenchmarks(benchDir)).filter((b) => b.kind === kind);
223
+ if (benchmarks.length === 0) {
224
+ console.log(pc.yellow(`No ${kind} benchmarks found in ${benchDir}`));
225
+ return;
226
+ }
227
+ const benchmarkId = await prompt.choice("Prompt", benchmarks.map((b) => ({
228
+ value: b.id, label: b.title, hint: b.description || b.id,
229
+ })), benchmarks[0].id);
230
+ const selectedBenchmark = benchmarks.find((b) => b.id === benchmarkId);
231
+ if (!selectedBenchmark) return;
232
+
233
+ const { backendFor } = await import("./backends.mjs");
234
+ const modelId = profile.modelAlias;
235
+ const modelSource = profile.providerId === "llama-cpp-mtp" ? "llama-cpp-mtp" : profile.backend === "ollama" ? "ollama" : profile.backend === "omlx" ? "omlx" : "llama-cpp";
236
+ const backendLabel = backendFor(profile.backend).label;
237
+
238
+ return await prepareBenchmarkRun({ repoPath, benchmark: selectedBenchmark, kind, modelId, modelSource, backendLabel, profile });
239
+ } finally {
240
+ prompt.close();
241
+ }
242
+ }
243
+
244
+ // ── Standalone benchmark flow (offgrid-ai benchmark) ──────────────────────
144
245
 
145
246
  export async function benchmarkFlow() {
146
247
  await ensureDirs();
@@ -150,13 +251,11 @@ export async function benchmarkFlow() {
150
251
  const repoPath = await linkBenchmarkRepo(prompt);
151
252
  if (!repoPath) return;
152
253
 
153
- // 1. Pick category
154
254
  const kind = await prompt.choice("Benchmark category", [
155
255
  { value: "visual", label: "Visual Benchmark", hint: "HTML/CSS/JS animation benchmarks" },
156
256
  { value: "data-science", label: "Data Science", hint: "Analysis and charting benchmarks" },
157
257
  ], "visual");
158
258
 
159
- // 2. Pick benchmark prompt
160
259
  const benchDir = join(repoPath, "benchmarks");
161
260
  const benchmarks = (await loadBenchmarks(benchDir)).filter((b) => b.kind === kind);
162
261
  if (benchmarks.length === 0) {
@@ -164,14 +263,11 @@ export async function benchmarkFlow() {
164
263
  return;
165
264
  }
166
265
  const benchmarkId = await prompt.choice("Prompt", benchmarks.map((b) => ({
167
- value: b.id,
168
- label: b.title,
169
- hint: b.description || b.id,
266
+ value: b.id, label: b.title, hint: b.description || b.id,
170
267
  })), benchmarks[0].id);
171
268
  const selectedBenchmark = benchmarks.find((b) => b.id === benchmarkId);
172
269
  if (!selectedBenchmark) return;
173
270
 
174
- // 3. Pick model source
175
271
  const { loadProfiles } = await import("./profiles.mjs");
176
272
  const { backendFor } = await import("./backends.mjs");
177
273
 
@@ -181,7 +277,7 @@ export async function benchmarkFlow() {
181
277
  { value: "cloud", label: "Custom / cloud", hint: "Free-form model label for cloud runs" },
182
278
  ], "profile");
183
279
 
184
- let modelId, modelSource, backendLabel;
280
+ let modelId, modelSource, backendLabel, profile;
185
281
 
186
282
  if (source === "profile") {
187
283
  if (profiles.length === 0) {
@@ -189,11 +285,9 @@ export async function benchmarkFlow() {
189
285
  return;
190
286
  }
191
287
  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}`,
288
+ value: p.id, label: p.label, hint: `${backendFor(p.backend).label} · ${p.modelAlias}`,
195
289
  })), profiles[0].id);
196
- const profile = profiles.find((p) => p.id === profileId);
290
+ profile = profiles.find((p) => p.id === profileId);
197
291
  if (!profile) return;
198
292
  modelId = profile.modelAlias;
199
293
  modelSource = profile.providerId === "llama-cpp-mtp" ? "llama-cpp-mtp" : profile.backend === "ollama" ? "ollama" : profile.backend === "omlx" ? "omlx" : "llama-cpp";
@@ -205,72 +299,7 @@ export async function benchmarkFlow() {
205
299
  modelSource = "cloud";
206
300
  }
207
301
 
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
- }
302
+ return await prepareBenchmarkRun({ repoPath, benchmark: selectedBenchmark, kind, modelId, modelSource, backendLabel, profile });
274
303
  } finally {
275
304
  prompt.close();
276
305
  }
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
  }