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 +1 -1
- package/src/benchmark.mjs +114 -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,113 @@ 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
|
+
...(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
|
-
|
|
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
|
-
|
|
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
|
}
|