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 +1 -1
- package/src/benchmark.mjs +110 -85
- package/src/cli.mjs +3 -0
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|