@hackerrank/astra-cli 0.1.1 → 0.1.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/README.md +25 -0
- package/package.json +1 -1
- package/src/cli.js +75 -22
- package/src/model.js +7 -1
- package/src/repl.js +69 -6
- package/src/report.html +569 -0
package/README.md
CHANGED
|
@@ -126,6 +126,29 @@ price entry shows **`n/a`** (never `$0`), and estimated costs (native routes) ar
|
|
|
126
126
|
marked `~`. `--push` requires an `s3://` URI and the `aws` CLI on PATH; if the
|
|
127
127
|
upload fails the local tarball is kept.
|
|
128
128
|
|
|
129
|
+
### HTML report
|
|
130
|
+
|
|
131
|
+
Every matrix run (and every single `-p`/`-t`/`-f` bench run) rebuilds
|
|
132
|
+
`bench/summary.json` and a self-contained `bench/report.html` dashboard —
|
|
133
|
+
no server, no build step, no external JS: open it straight from `file://` or
|
|
134
|
+
from inside the `bench-*.tgz` tarball. It has a leaderboard, a model × task
|
|
135
|
+
pass-rate heatmap, cost/token charts, per-step trend lines, and an
|
|
136
|
+
expandable command timeline for every run.
|
|
137
|
+
|
|
138
|
+
Regenerate it on demand (e.g. after manually editing/pruning `bench/`) with:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
astra --report # rebuild ./bench/{summary.json,report.html}
|
|
142
|
+
astra --report --bench-root ./other-bench
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`summary.json` follows the `astra-bench-1` schema: `kpis` (totals), a tidy
|
|
146
|
+
`leaderboard[]` (one row per model×reasoning), `matrix[]` (model×reasoning×task
|
|
147
|
+
pass@k), `runs[]` (every attempt, with a compact per-step `timeline`), and
|
|
148
|
+
`step_series[]` (token/cost distributions by step, for the trend charts). Each
|
|
149
|
+
run folder also gets a standalone `run.json` for drill-down without loading
|
|
150
|
+
the full `trajectory.json`.
|
|
151
|
+
|
|
129
152
|
### Sessions
|
|
130
153
|
|
|
131
154
|
Every run (either mode) is saved under `~/.astra/sessions/<id>.json`.
|
|
@@ -153,6 +176,8 @@ credits — that's a quota issue, not a bug.
|
|
|
153
176
|
--bench-root <dir> Root folder for bench runs (default: ./bench)
|
|
154
177
|
--tar After a matrix, write a bench/ tarball locally
|
|
155
178
|
--push <s3-uri> After a matrix, tar bench/ and `aws s3 cp` to the URI
|
|
179
|
+
--report Rebuild bench/summary.json + bench/report.html from
|
|
180
|
+
whatever runs already exist on disk, then exit
|
|
156
181
|
-C, --cwd <path> Working directory for commands (default: cwd)
|
|
157
182
|
-o, --output <path> Also write trajectory JSON here (autonomous mode)
|
|
158
183
|
-s, --steps <n> Step limit (default: 40)
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
* s3://astra-bench-results/<timestamp>/
|
|
33
33
|
* --push-uri <s3-uri> Override the S3 push destination
|
|
34
34
|
* --tar After a matrix, write a bench/ tarball locally
|
|
35
|
+
* --report Rebuild bench/summary.json + bench/report.html
|
|
36
|
+
* from whatever runs already exist on disk, then
|
|
37
|
+
* exit (no model/API key needed).
|
|
35
38
|
* -t, --task <text> Task text -> bench mode (run to completion)
|
|
36
39
|
* -f, --task-file <path> Read task text from a file -> bench mode
|
|
37
40
|
* -p, --path <dir> Task directory (copied into an isolated bench
|
|
@@ -94,6 +97,7 @@ import {
|
|
|
94
97
|
defaultPushUri,
|
|
95
98
|
benchRoot,
|
|
96
99
|
} from "./bench.js";
|
|
100
|
+
import { refreshReport } from "./report.js";
|
|
97
101
|
|
|
98
102
|
/** Map a reasoning level to gateway modelKwargs. Empty for off/none/unset. */
|
|
99
103
|
function reasoningKwargs(level) {
|
|
@@ -115,6 +119,7 @@ function parseArgs(argv) {
|
|
|
115
119
|
"--push": "push",
|
|
116
120
|
"--push-uri": "push-uri",
|
|
117
121
|
"--tar": "tar",
|
|
122
|
+
"--report": "report",
|
|
118
123
|
"--bench-root": "bench-root",
|
|
119
124
|
"-C": "cwd", "--cwd": "cwd",
|
|
120
125
|
"-o": "output", "--output": "output",
|
|
@@ -130,7 +135,7 @@ function parseArgs(argv) {
|
|
|
130
135
|
"-y": "yolo", "--yolo": "yolo",
|
|
131
136
|
"-h": "help", "--help": "help",
|
|
132
137
|
};
|
|
133
|
-
const flags = new Set(["quiet", "yolo", "help", "sessions", "tar", "push"]);
|
|
138
|
+
const flags = new Set(["quiet", "yolo", "help", "sessions", "tar", "push", "report"]);
|
|
134
139
|
for (let i = 2; i < argv.length; i++) {
|
|
135
140
|
const key = alias[argv[i]];
|
|
136
141
|
if (!key) { console.error(`Unknown option: ${argv[i]}`); process.exit(2); }
|
|
@@ -154,6 +159,18 @@ async function main() {
|
|
|
154
159
|
|
|
155
160
|
if (args.sessions) { printSessions(); process.exit(0); }
|
|
156
161
|
if (args.help) { console.log(HELP); process.exit(0); }
|
|
162
|
+
if (args.report) {
|
|
163
|
+
try {
|
|
164
|
+
const res = refreshReport(args["bench-root"]);
|
|
165
|
+
console.error(`\x1b[1m[astra] report · ${res.runs} run(s) · ${res.models} model(s) · ${res.tasks} task(s)\x1b[0m`);
|
|
166
|
+
console.error(`\x1b[1m summary\x1b[0m ${res.summary}`);
|
|
167
|
+
console.error(`\x1b[1m html \x1b[0m ${res.html}`);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error(`\x1b[31m[astra] report failed: ${err.message}\x1b[0m`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
157
174
|
|
|
158
175
|
// Load a session to resume (if any) to infer defaults.
|
|
159
176
|
let resumeDoc = null;
|
|
@@ -384,6 +401,12 @@ async function main() {
|
|
|
384
401
|
console.error(`\x1b[2m[astra] metrics -> ${paths.runMetrics}\x1b[0m`);
|
|
385
402
|
console.error(`\x1b[2m[astra] index -> ${paths.index}\x1b[0m`);
|
|
386
403
|
}
|
|
404
|
+
try {
|
|
405
|
+
const report = refreshReport(benchRun.root);
|
|
406
|
+
if (!quiet) console.error(`\x1b[2m[astra] report -> ${report.html}\x1b[0m`);
|
|
407
|
+
} catch {
|
|
408
|
+
// Non-fatal: report regeneration is best-effort for single runs.
|
|
409
|
+
}
|
|
387
410
|
}
|
|
388
411
|
|
|
389
412
|
if (!quiet) {
|
|
@@ -408,9 +431,50 @@ async function main() {
|
|
|
408
431
|
console.error(`\x1b[2m[astra] session -> ${sessionId}\x1b[0m`);
|
|
409
432
|
}
|
|
410
433
|
if (result.submission) console.log(result.submission);
|
|
434
|
+
|
|
435
|
+
// For a single-run bench, tar + push the bench/ folder if requested.
|
|
436
|
+
// (Matrix runs handle this inside runBenchMatrix.)
|
|
437
|
+
if (benchRun) {
|
|
438
|
+
const links = [];
|
|
439
|
+
archiveAndPush(args, benchRun.root, links);
|
|
440
|
+
if (links.length) {
|
|
441
|
+
const w = Math.max(...links.map(([k]) => k.length));
|
|
442
|
+
console.error("");
|
|
443
|
+
for (const [k, v] of links) console.error(`\x1b[1m ${k.padEnd(w)}\x1b[0m ${v}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
411
447
|
process.exit(result.exit_status === "Submitted" ? 0 : 1);
|
|
412
448
|
}
|
|
413
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Tar the bench/ folder and (optionally) upload it to S3. Mutates `links`
|
|
452
|
+
* with the resulting artifact paths/URLs and prints any failures.
|
|
453
|
+
* Works for both single-run and matrix bench runs.
|
|
454
|
+
*/
|
|
455
|
+
function archiveAndPush(args, root, links) {
|
|
456
|
+
if (!(args.tar || args.push)) return;
|
|
457
|
+
try {
|
|
458
|
+
const tarball = packBench({ root });
|
|
459
|
+
links.push(["tarball", tarball]);
|
|
460
|
+
if (args.push) {
|
|
461
|
+
// Bare --push uses the default bucket + timestamp prefix; --push-uri
|
|
462
|
+
// overrides the destination.
|
|
463
|
+
const dest = args["push-uri"] || defaultPushUri();
|
|
464
|
+
try {
|
|
465
|
+
const pushed = pushToS3(tarball, dest);
|
|
466
|
+
links.push(["s3 uri", pushed.uri]);
|
|
467
|
+
links.push(["s3 url", pushed.url]);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
console.error(`\x1b[31m[astra] S3 push failed: ${err.message}\x1b[0m`);
|
|
470
|
+
console.error(`\x1b[33m[astra] tarball kept locally: ${tarball}\x1b[0m`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch (err) {
|
|
474
|
+
console.error(`\x1b[31m[astra] tar failed: ${err.message}\x1b[0m`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
414
478
|
/**
|
|
415
479
|
* Run a multi-model / multi-reasoning / repeated bench matrix, print a
|
|
416
480
|
* leaderboard, optionally tar + upload the bench/ folder, then exit.
|
|
@@ -463,29 +527,18 @@ async function runBenchMatrix({ args, apiKey, models, reasonings, repeat, task,
|
|
|
463
527
|
// Collect the important artifact locations to print together at the end.
|
|
464
528
|
const links = [["metrics", indexPath]];
|
|
465
529
|
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// overrides the destination.
|
|
474
|
-
const dest = args["push-uri"] || defaultPushUri();
|
|
475
|
-
try {
|
|
476
|
-
const pushed = pushToS3(tarball, dest);
|
|
477
|
-
links.push(["s3 uri", pushed.uri]);
|
|
478
|
-
links.push(["s3 url", pushed.url]);
|
|
479
|
-
} catch (err) {
|
|
480
|
-
console.error(`\x1b[31m[astra] S3 push failed: ${err.message}\x1b[0m`);
|
|
481
|
-
console.error(`\x1b[33m[astra] tarball kept locally: ${tarball}\x1b[0m`);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
} catch (err) {
|
|
485
|
-
console.error(`\x1b[31m[astra] tar failed: ${err.message}\x1b[0m`);
|
|
486
|
-
}
|
|
530
|
+
// Rebuild the dashboard (summary.json + report.html) from every run on
|
|
531
|
+
// disk under this root, including runs from earlier matrix invocations.
|
|
532
|
+
try {
|
|
533
|
+
const report = refreshReport(root);
|
|
534
|
+
links.push(["report", report.html]);
|
|
535
|
+
} catch (err) {
|
|
536
|
+
console.error(`\x1b[33m[astra] report generation skipped: ${err.message}\x1b[0m`);
|
|
487
537
|
}
|
|
488
538
|
|
|
539
|
+
// Tar + optional S3 push of the whole bench/ folder.
|
|
540
|
+
archiveAndPush(args, root, links);
|
|
541
|
+
|
|
489
542
|
const solved = rows.filter((r) => r.resolved).length;
|
|
490
543
|
console.error(`\n\x1b[1m[astra] matrix done · ${solved}/${rows.length} resolved\x1b[0m`);
|
|
491
544
|
|
package/src/model.js
CHANGED
|
@@ -67,6 +67,9 @@ export class GatewayModel {
|
|
|
67
67
|
this.costSource = null; // "reported" | "estimated" | "mixed" | null
|
|
68
68
|
// Preferred token-cap parameter; swapped automatically on 400 if needed.
|
|
69
69
|
this._tokenParam = "max_tokens";
|
|
70
|
+
// Optional AbortSignal; when set and aborted, in-flight requests are
|
|
71
|
+
// cancelled (used to force-quit a long model call on a second Ctrl+C).
|
|
72
|
+
this.signal = null;
|
|
70
73
|
if (!this.apiKey) {
|
|
71
74
|
throw new Error(
|
|
72
75
|
"GatewayModel: no API key. Set ASTRA_GATEWAY_API_KEY or configure ~/.astra/config.json."
|
|
@@ -100,6 +103,7 @@ export class GatewayModel {
|
|
|
100
103
|
Authorization: `Bearer ${this.apiKey}`,
|
|
101
104
|
},
|
|
102
105
|
body: JSON.stringify(body),
|
|
106
|
+
signal: this.signal || undefined,
|
|
103
107
|
});
|
|
104
108
|
|
|
105
109
|
if (!res.ok) {
|
|
@@ -228,8 +232,10 @@ export class GatewayModel {
|
|
|
228
232
|
|
|
229
233
|
function isRetryable(err) {
|
|
230
234
|
// Never retry classified terminal errors (auth/quota/bad-request) or context
|
|
231
|
-
// overflow — those won't succeed on retry.
|
|
235
|
+
// overflow — those won't succeed on retry. Also never retry an aborted
|
|
236
|
+
// request (user force-quit via Ctrl+C).
|
|
232
237
|
if (err instanceof GatewayError || err instanceof ContextWindowError) return false;
|
|
238
|
+
if (err?.name === "AbortError") return false;
|
|
233
239
|
const msg = String(err?.message || err);
|
|
234
240
|
return /HTTP 5\d\d|HTTP 429|ECONNRESET|ETIMEDOUT|fetch failed|network/i.test(msg);
|
|
235
241
|
}
|
package/src/repl.js
CHANGED
|
@@ -134,7 +134,9 @@ function turnSummary({ turn, upTok, downTok, costUsd, costKind, seconds }) {
|
|
|
134
134
|
*/
|
|
135
135
|
function footerLines(agent, model, yolo, mode = "agent") {
|
|
136
136
|
const cols = process.stdout.columns || 100;
|
|
137
|
-
|
|
137
|
+
// Leave one cell of slack so a full-width rule never wraps to a new row
|
|
138
|
+
// (which pushes the whole footer up and looks broken on some terminals).
|
|
139
|
+
const rule = "─".repeat(Math.max(1, cols - 1));
|
|
138
140
|
|
|
139
141
|
const reasoning = model.modelKwargs?.reasoning_effort || model.modelKwargs?.reasoning?.effort || "off";
|
|
140
142
|
const modeTag = mode === "bench" ? C.yellow("◆ bench") : C.cyan("◆ agent");
|
|
@@ -154,12 +156,13 @@ function footerLines(agent, model, yolo, mode = "agent") {
|
|
|
154
156
|
|
|
155
157
|
return [
|
|
156
158
|
C.dim(rule),
|
|
157
|
-
modeTag + " " + C.cyan(repoLabel()) + " " + C.dim(modelBits.join(" "))
|
|
159
|
+
modeTag + " " + C.cyan(repoLabel()) + " " + C.dim(modelBits.join(" ")),
|
|
158
160
|
C.dim(usage),
|
|
161
|
+
C.dim("opt+left/right: model · opt+up/down: reasoning · shift+tab: mode"),
|
|
159
162
|
];
|
|
160
163
|
}
|
|
161
164
|
|
|
162
|
-
const FOOTER_LINES =
|
|
165
|
+
const FOOTER_LINES = 4; // divider + repo/model + usage + shortcuts
|
|
163
166
|
const PROMPT_PREFIX = C.green("› "); // live input row (minimal)
|
|
164
167
|
const HISTORY_PREFIX = C.green("you › "); // echoed into transcript/history
|
|
165
168
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -336,6 +339,11 @@ class Screen {
|
|
|
336
339
|
this.drawPrompt();
|
|
337
340
|
}
|
|
338
341
|
|
|
342
|
+
/** True while the agent is working (a turn is in progress). */
|
|
343
|
+
isBusy() {
|
|
344
|
+
return this._busy || this._busyResume;
|
|
345
|
+
}
|
|
346
|
+
|
|
339
347
|
/**
|
|
340
348
|
* Resolve with the next full line the user types. An optional prompt label
|
|
341
349
|
* replaces the default "you › " (used by the approval gate so the question
|
|
@@ -486,8 +494,12 @@ class Screen {
|
|
|
486
494
|
this.buf = this.buf.slice(0, -1);
|
|
487
495
|
this.drawPrompt();
|
|
488
496
|
} else if (ch === "\x03") { // Ctrl+C
|
|
497
|
+
// If a prompt is pending (idle), resolve it so the loop can react.
|
|
498
|
+
// Also always notify the interrupt hook so a *running* agent turn can
|
|
499
|
+
// be aborted even when no readLine is pending.
|
|
489
500
|
const r = this.resolve; this.resolve = null;
|
|
490
501
|
if (r) r("__SIGINT__");
|
|
502
|
+
if (this.onInterrupt) this.onInterrupt();
|
|
491
503
|
} else if (ch === "\x04") { // Ctrl+D
|
|
492
504
|
const r = this.resolve; this.resolve = null;
|
|
493
505
|
if (r) r("__EOF__");
|
|
@@ -576,6 +588,31 @@ async function runStickyRepl(agent, model, yolo, intro = [], { runBench = null }
|
|
|
576
588
|
// extra needed here, but expose a hook for symmetry / future use).
|
|
577
589
|
screen.onClearInput = () => {};
|
|
578
590
|
|
|
591
|
+
// Ctrl+C handling. In raw mode Ctrl+C does NOT raise SIGINT — it arrives as
|
|
592
|
+
// a byte, so we handle it explicitly. The screen's key handler already
|
|
593
|
+
// resolves any pending readLine with "__SIGINT__" (idle case); here we only
|
|
594
|
+
// deal with the *busy* case where no readLine is pending:
|
|
595
|
+
// - first Ctrl+C while working -> request an abort after the current step
|
|
596
|
+
// - second Ctrl+C while working -> force an immediate quit
|
|
597
|
+
const interrupt = { aborted: false, quit: false };
|
|
598
|
+
screen.onInterrupt = () => {
|
|
599
|
+
if (!screen.isBusy()) {
|
|
600
|
+
// Idle: the readLine resolver was already fired with "__SIGINT__", which
|
|
601
|
+
// breaks the loop and quits gracefully. Nothing more to do here.
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (interrupt.aborted) {
|
|
605
|
+
// Second Ctrl+C while working -> force quit: abort the in-flight model
|
|
606
|
+
// request so a long call unblocks immediately.
|
|
607
|
+
interrupt.quit = true;
|
|
608
|
+
log(C.red("[astra] force quitting…"));
|
|
609
|
+
if (model._abort) { try { model._abort.abort(); } catch {} }
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
interrupt.aborted = true;
|
|
613
|
+
log(C.yellow("[astra] interrupting… (press Ctrl+C again to force quit)"));
|
|
614
|
+
};
|
|
615
|
+
|
|
579
616
|
// Approval gate: show the command, then ask for y/n/a on the prompt row.
|
|
580
617
|
agent.confirm = async (command) => {
|
|
581
618
|
log(C.yellow("$ " + command));
|
|
@@ -621,6 +658,7 @@ async function runStickyRepl(agent, model, yolo, intro = [], { runBench = null }
|
|
|
621
658
|
mode = "agent"; refresh(); continue;
|
|
622
659
|
}
|
|
623
660
|
screen.startBusy("benching");
|
|
661
|
+
interrupt.aborted = false;
|
|
624
662
|
try {
|
|
625
663
|
await runBench(line, log);
|
|
626
664
|
} catch (err) {
|
|
@@ -628,18 +666,33 @@ async function runStickyRepl(agent, model, yolo, intro = [], { runBench = null }
|
|
|
628
666
|
} finally {
|
|
629
667
|
screen.stopBusy();
|
|
630
668
|
}
|
|
669
|
+
if (interrupt.quit) break;
|
|
631
670
|
mode = "agent";
|
|
632
671
|
refresh();
|
|
633
672
|
continue;
|
|
634
673
|
}
|
|
635
674
|
|
|
636
675
|
agent.addUserMessage(line);
|
|
676
|
+
interrupt.aborted = false; // reset per turn
|
|
677
|
+
// Fresh AbortController for this turn so a force-quit can cancel an
|
|
678
|
+
// in-flight model request. Cleared in `finally`.
|
|
679
|
+
model._abort = new AbortController();
|
|
680
|
+
model.signal = model._abort.signal;
|
|
637
681
|
screen.startBusy("working");
|
|
638
682
|
try {
|
|
639
|
-
await driveUntilChat(agent, log);
|
|
683
|
+
await driveUntilChat(agent, log, interrupt);
|
|
684
|
+
} catch (err) {
|
|
685
|
+
if (err?.name === "AbortError" || interrupt.quit) {
|
|
686
|
+
// Force-quit path: swallow the abort and fall through to break.
|
|
687
|
+
} else {
|
|
688
|
+
throw err;
|
|
689
|
+
}
|
|
640
690
|
} finally {
|
|
641
691
|
screen.stopBusy();
|
|
692
|
+
model.signal = null;
|
|
693
|
+
model._abort = null;
|
|
642
694
|
}
|
|
695
|
+
if (interrupt.quit) break; // force-quit requested via a second Ctrl+C
|
|
643
696
|
const after = usageSnapshot(model);
|
|
644
697
|
log(
|
|
645
698
|
turnSummary({
|
|
@@ -733,10 +786,16 @@ function usageSnapshot(model) {
|
|
|
733
786
|
|
|
734
787
|
/**
|
|
735
788
|
* Take turns until the agent produces a chat reply (or terminates). Commands
|
|
736
|
-
* run in between; their output goes back to the model automatically.
|
|
789
|
+
* run in between; their output goes back to the model automatically. If an
|
|
790
|
+
* `interrupt` signal is provided, the loop stops cleanly between turns when
|
|
791
|
+
* `interrupt.aborted` becomes true (Ctrl+C while working).
|
|
737
792
|
*/
|
|
738
|
-
async function driveUntilChat(agent, log) {
|
|
793
|
+
async function driveUntilChat(agent, log, interrupt = null) {
|
|
739
794
|
while (true) {
|
|
795
|
+
if (interrupt && interrupt.aborted) {
|
|
796
|
+
log(C.yellow("[astra] interrupted — returning to prompt."));
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
740
799
|
let turn;
|
|
741
800
|
try {
|
|
742
801
|
turn = await agent.runTurn();
|
|
@@ -748,6 +807,10 @@ async function driveUntilChat(agent, log) {
|
|
|
748
807
|
throw err;
|
|
749
808
|
}
|
|
750
809
|
|
|
810
|
+
if (interrupt && interrupt.aborted) {
|
|
811
|
+
log(C.yellow("[astra] interrupted — returning to prompt."));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
751
814
|
if (turn.kind === "chat") {
|
|
752
815
|
log("\n" + C.cyan("astra › ") + turn.content.trim());
|
|
753
816
|
return;
|
package/src/report.html
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>astra bench report</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0f1115;
|
|
10
|
+
--panel: #171a21;
|
|
11
|
+
--panel-2: #1d212b;
|
|
12
|
+
--border: #262b36;
|
|
13
|
+
--text: #e6e8ee;
|
|
14
|
+
--muted: #8b93a7;
|
|
15
|
+
--accent: #5b9dff;
|
|
16
|
+
--good: #3ecf8e;
|
|
17
|
+
--bad: #ef5a6f;
|
|
18
|
+
--warn: #e8b339;
|
|
19
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
20
|
+
}
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
body { margin: 0; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.45; }
|
|
23
|
+
a { color: var(--accent); }
|
|
24
|
+
.wrap { max-width: 1180px; margin: 0 auto; padding: 28px 20px 64px; }
|
|
25
|
+
header.top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; flex-wrap: wrap; gap: 8px; }
|
|
26
|
+
header.top h1 { font-size: 20px; margin: 0; font-weight: 650; letter-spacing: .2px; }
|
|
27
|
+
header.top .meta { color: var(--muted); font-size: 12.5px; }
|
|
28
|
+
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin: 20px 0 28px; }
|
|
29
|
+
.kpi { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; }
|
|
30
|
+
.kpi .v { font-size: 22px; font-weight: 700; }
|
|
31
|
+
.kpi .l { color: var(--muted); font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em; margin-top: 2px; }
|
|
32
|
+
section { margin: 34px 0; }
|
|
33
|
+
section > h2 { font-size: 15px; margin: 0 0 12px; font-weight: 650; color: var(--text); display: flex; align-items: center; gap: 8px; }
|
|
34
|
+
section > h2 .sub { color: var(--muted); font-weight: 400; font-size: 12px; }
|
|
35
|
+
.grid2 { display: grid; grid-template-columns: 1.1fr 1fr; gap: 16px; }
|
|
36
|
+
.grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
|
|
37
|
+
@media (max-width: 900px) { .grid2, .grid3 { grid-template-columns: 1fr; } }
|
|
38
|
+
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; }
|
|
39
|
+
.panel h3 { margin: 0 0 10px; font-size: 12.5px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .05em; }
|
|
40
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
41
|
+
th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
42
|
+
th { color: var(--muted); font-weight: 600; font-size: 11.5px; text-transform: uppercase; letter-spacing: .03em; cursor: pointer; user-select: none; }
|
|
43
|
+
th:hover { color: var(--text); }
|
|
44
|
+
th .arrow { opacity: .5; font-size: 10px; margin-left: 3px; }
|
|
45
|
+
tbody tr:hover { background: rgba(255,255,255,0.02); }
|
|
46
|
+
tbody tr.run-row { cursor: pointer; }
|
|
47
|
+
.pill { display: inline-block; padding: 1px 8px; border-radius: 100px; font-size: 11px; font-weight: 600; }
|
|
48
|
+
.pill.ok { background: rgba(62,207,142,.15); color: var(--good); }
|
|
49
|
+
.pill.bad { background: rgba(239,90,111,.15); color: var(--bad); }
|
|
50
|
+
.pill.warn { background: rgba(232,179,57,.15); color: var(--warn); }
|
|
51
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
|
|
52
|
+
.muted { color: var(--muted); }
|
|
53
|
+
.bar-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; }
|
|
54
|
+
.bar-row .lbl { width: 150px; flex: 0 0 150px; font-size: 12px; overflow: hidden; text-overflow: ellipsis; }
|
|
55
|
+
.bar-track { flex: 1; height: 14px; background: var(--panel-2); border-radius: 6px; overflow: hidden; display: flex; }
|
|
56
|
+
.bar-seg { height: 100%; }
|
|
57
|
+
.bar-val { width: 64px; flex: 0 0 64px; text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
58
|
+
.legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 10px; font-size: 11.5px; color: var(--muted); }
|
|
59
|
+
.legend .sw { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 5px; vertical-align: -1px; }
|
|
60
|
+
.heat-table td { text-align: center; font-variant-numeric: tabular-nums; }
|
|
61
|
+
.heat-cell { display: inline-block; min-width: 44px; padding: 3px 6px; border-radius: 5px; font-size: 12px; }
|
|
62
|
+
details.run-detail { margin: 0; }
|
|
63
|
+
details.run-detail > summary { display: none; }
|
|
64
|
+
.timeline { margin: 0 0 18px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
65
|
+
.timeline .step { padding: 10px 14px; border-bottom: 1px solid var(--border); }
|
|
66
|
+
.timeline .step:last-child { border-bottom: none; }
|
|
67
|
+
.timeline .step .head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; margin-bottom: 4px; }
|
|
68
|
+
.timeline .step .n { color: var(--accent); font-weight: 700; font-size: 12px; }
|
|
69
|
+
.timeline .step .stats { color: var(--muted); font-size: 11.5px; }
|
|
70
|
+
.timeline .step .thought { color: var(--muted); font-size: 12.5px; margin-bottom: 6px; }
|
|
71
|
+
.timeline .step pre { background: var(--panel-2); border-radius: 6px; padding: 8px 10px; margin: 4px 0; font-size: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-word; }
|
|
72
|
+
.rc0 { color: var(--good); } .rcN { color: var(--bad); }
|
|
73
|
+
.toolbar { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
|
|
74
|
+
.toolbar select, .toolbar input { background: var(--panel-2); border: 1px solid var(--border); color: var(--text); border-radius: 6px; padding: 5px 9px; font-size: 12.5px; }
|
|
75
|
+
.toolbar label { color: var(--muted); font-size: 12px; }
|
|
76
|
+
footer { color: var(--muted); font-size: 11.5px; margin-top: 40px; text-align: center; }
|
|
77
|
+
svg text { fill: var(--muted); font-size: 10.5px; }
|
|
78
|
+
.axis line, .axis path { stroke: var(--border); }
|
|
79
|
+
.n-a { color: var(--muted); }
|
|
80
|
+
</style>
|
|
81
|
+
</head>
|
|
82
|
+
<body>
|
|
83
|
+
<div class="wrap">
|
|
84
|
+
<header class="top">
|
|
85
|
+
<h1>astra bench report</h1>
|
|
86
|
+
<div class="meta" id="generated"></div>
|
|
87
|
+
</header>
|
|
88
|
+
|
|
89
|
+
<div class="kpis" id="kpis"></div>
|
|
90
|
+
|
|
91
|
+
<section>
|
|
92
|
+
<h2>Leaderboard <span class="sub">one row per model · reasoning</span></h2>
|
|
93
|
+
<div class="grid2">
|
|
94
|
+
<div class="panel">
|
|
95
|
+
<h3>Solve rate</h3>
|
|
96
|
+
<div id="chart-solve"></div>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="panel">
|
|
99
|
+
<h3>Outcomes</h3>
|
|
100
|
+
<div id="chart-outcomes"></div>
|
|
101
|
+
<div class="legend" id="legend-outcomes"></div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="panel" style="margin-top:16px;">
|
|
105
|
+
<table id="tbl-leaderboard"></table>
|
|
106
|
+
</div>
|
|
107
|
+
</section>
|
|
108
|
+
|
|
109
|
+
<section>
|
|
110
|
+
<h2>Cost & tokens</h2>
|
|
111
|
+
<div class="grid2">
|
|
112
|
+
<div class="panel">
|
|
113
|
+
<h3>Cost vs. solve rate</h3>
|
|
114
|
+
<div id="chart-scatter"></div>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="panel">
|
|
117
|
+
<h3>Token mix (prompt / completion / reasoning / cached)</h3>
|
|
118
|
+
<div id="chart-tokens"></div>
|
|
119
|
+
<div class="legend" id="legend-tokens"></div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
|
|
124
|
+
<section id="section-series">
|
|
125
|
+
<h2>Per-step trend <span class="sub">median tokens & cumulative cost across the trajectory</span></h2>
|
|
126
|
+
<div class="grid2">
|
|
127
|
+
<div class="panel">
|
|
128
|
+
<h3>Prompt tokens (context) vs. step</h3>
|
|
129
|
+
<div id="chart-line-tokens"></div>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="panel">
|
|
132
|
+
<h3>Cumulative cost vs. step</h3>
|
|
133
|
+
<div id="chart-line-cost"></div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="legend" id="legend-series"></div>
|
|
137
|
+
</section>
|
|
138
|
+
|
|
139
|
+
<section id="section-matrix">
|
|
140
|
+
<h2>Model × task <span class="sub">pass@k</span></h2>
|
|
141
|
+
<div class="panel">
|
|
142
|
+
<table id="tbl-matrix" class="heat-table"></table>
|
|
143
|
+
</div>
|
|
144
|
+
</section>
|
|
145
|
+
|
|
146
|
+
<section>
|
|
147
|
+
<h2>Runs <span class="sub">every attempt · click a row to expand the command timeline</span></h2>
|
|
148
|
+
<div class="toolbar">
|
|
149
|
+
<label>Model
|
|
150
|
+
<select id="filter-model"><option value="">all</option></select>
|
|
151
|
+
</label>
|
|
152
|
+
<label>Task
|
|
153
|
+
<select id="filter-task"><option value="">all</option></select>
|
|
154
|
+
</label>
|
|
155
|
+
<label>Outcome
|
|
156
|
+
<select id="filter-outcome"><option value="">all</option></select>
|
|
157
|
+
</label>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="panel">
|
|
160
|
+
<table id="tbl-runs"></table>
|
|
161
|
+
</div>
|
|
162
|
+
</section>
|
|
163
|
+
|
|
164
|
+
<footer>generated by <span class="mono">astra bench --report</span> · schema <span id="schema"></span></footer>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<script>
|
|
168
|
+
/*__ASTRA_DATA__*/
|
|
169
|
+
</script>
|
|
170
|
+
<script>
|
|
171
|
+
(function () {
|
|
172
|
+
var DATA = window.DATA || { kpis: {}, leaderboard: [], matrix: [], runs: [], step_series: [] };
|
|
173
|
+
var PALETTE = ["#5b9dff", "#3ecf8e", "#e8b339", "#ef5a6f", "#b57bff", "#33c2c2", "#ff8a5b", "#a3d977"];
|
|
174
|
+
var colorCache = {};
|
|
175
|
+
function colorFor(key) {
|
|
176
|
+
if (!colorCache[key]) {
|
|
177
|
+
var n = Object.keys(colorCache).length;
|
|
178
|
+
colorCache[key] = PALETTE[n % PALETTE.length];
|
|
179
|
+
}
|
|
180
|
+
return colorCache[key];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function fmt(n) { return Number(n || 0).toLocaleString("en-US"); }
|
|
184
|
+
function fmt1(n) { return (Number(n) || 0).toFixed(1); }
|
|
185
|
+
function pct(n) { return Math.round((Number(n) || 0) * 100) + "%"; }
|
|
186
|
+
function fmtUsd(n) {
|
|
187
|
+
if (n == null) return '<span class="n-a">n/a</span>';
|
|
188
|
+
var v = Number(n) || 0;
|
|
189
|
+
if (v === 0) return "$0";
|
|
190
|
+
if (v < 0.01) return "$" + v.toFixed(5);
|
|
191
|
+
if (v < 1) return "$" + v.toFixed(4);
|
|
192
|
+
return "$" + v.toFixed(2);
|
|
193
|
+
}
|
|
194
|
+
function esc(s) {
|
|
195
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
|
196
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function el(tag, attrs, html) {
|
|
200
|
+
var e = document.createElement(tag);
|
|
201
|
+
for (var k in attrs || {}) e.setAttribute(k, attrs[k]);
|
|
202
|
+
if (html != null) e.innerHTML = html;
|
|
203
|
+
return e;
|
|
204
|
+
}
|
|
205
|
+
function svgEl(tag, attrs) {
|
|
206
|
+
var e = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
207
|
+
for (var k in attrs || {}) e.setAttribute(k, attrs[k]);
|
|
208
|
+
return e;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------- header
|
|
212
|
+
document.getElementById("generated").textContent = "generated " + (DATA.generated_at || "");
|
|
213
|
+
document.getElementById("schema").textContent = DATA.schema || "";
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------- KPIs
|
|
216
|
+
(function renderKpis() {
|
|
217
|
+
var k = DATA.kpis || {};
|
|
218
|
+
var cards = [
|
|
219
|
+
["Runs", fmt(k.runs)],
|
|
220
|
+
["Models", fmt(k.models)],
|
|
221
|
+
["Tasks", fmt(k.tasks)],
|
|
222
|
+
["Solved", fmt(k.solved) + " / " + fmt(k.runs)],
|
|
223
|
+
["Solve rate", pct(k.solved_rate)],
|
|
224
|
+
["Total tokens", fmt(k.tokens)],
|
|
225
|
+
["Total cost", fmtUsd(k.cost_usd) + (k.cost_source === "estimated" ? "~" : "")],
|
|
226
|
+
["Elapsed", fmt(k.elapsed_seconds) + "s"],
|
|
227
|
+
];
|
|
228
|
+
var host = document.getElementById("kpis");
|
|
229
|
+
cards.forEach(function (c) {
|
|
230
|
+
host.appendChild(el("div", { class: "kpi" }, '<div class="v">' + c[1] + '</div><div class="l">' + esc(c[0]) + "</div>"));
|
|
231
|
+
});
|
|
232
|
+
})();
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------- sortable table helper
|
|
235
|
+
function sortableTable(container, columns, rows, opts) {
|
|
236
|
+
opts = opts || {};
|
|
237
|
+
var state = { key: opts.defaultKey || columns[0].key, dir: opts.defaultDir || -1 };
|
|
238
|
+
function draw() {
|
|
239
|
+
var sorted = rows.slice().sort(function (a, b) {
|
|
240
|
+
var va = a[state.key], vb = b[state.key];
|
|
241
|
+
if (typeof va === "string" || typeof vb === "string") {
|
|
242
|
+
return state.dir * String(va).localeCompare(String(vb));
|
|
243
|
+
}
|
|
244
|
+
return state.dir * ((Number(va) || 0) - (Number(vb) || 0));
|
|
245
|
+
});
|
|
246
|
+
var thead = "<thead><tr>" + columns.map(function (c) {
|
|
247
|
+
var arrow = c.key === state.key ? (state.dir === 1 ? "\u2191" : "\u2193") : "";
|
|
248
|
+
return '<th data-key="' + c.key + '">' + esc(c.label) + (arrow ? ' <span class="arrow">' + arrow + "</span>" : "") + "</th>";
|
|
249
|
+
}).join("") + "</tr></thead>";
|
|
250
|
+
var tbody = "<tbody>" + sorted.map(function (row, i) {
|
|
251
|
+
var cells = columns.map(function (c) { return "<td>" + c.render(row) + "</td>"; }).join("");
|
|
252
|
+
var extra = opts.rowAttrs ? opts.rowAttrs(row, i) : "";
|
|
253
|
+
return "<tr " + extra + ">" + cells + "</tr>";
|
|
254
|
+
}).join("") + "</tbody>";
|
|
255
|
+
container.innerHTML = thead + tbody;
|
|
256
|
+
container.querySelectorAll("th").forEach(function (th) {
|
|
257
|
+
th.addEventListener("click", function () {
|
|
258
|
+
var key = th.getAttribute("data-key");
|
|
259
|
+
if (state.key === key) state.dir *= -1;
|
|
260
|
+
else { state.key = key; state.dir = -1; }
|
|
261
|
+
draw();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
if (opts.afterDraw) opts.afterDraw(container, sorted);
|
|
265
|
+
}
|
|
266
|
+
draw();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------- leaderboard chart: solve rate bars
|
|
270
|
+
(function () {
|
|
271
|
+
var host = document.getElementById("chart-solve");
|
|
272
|
+
(DATA.leaderboard || []).forEach(function (g) {
|
|
273
|
+
var row = el("div", { class: "bar-row" });
|
|
274
|
+
row.appendChild(el("div", { class: "lbl mono" }, esc(g.slug)));
|
|
275
|
+
var track = el("div", { class: "bar-track" });
|
|
276
|
+
var seg = el("div", {
|
|
277
|
+
class: "bar-seg",
|
|
278
|
+
style: "width:" + Math.round((g.solved_rate || 0) * 100) + "%;background:" + colorFor(g.slug),
|
|
279
|
+
});
|
|
280
|
+
track.appendChild(seg);
|
|
281
|
+
row.appendChild(track);
|
|
282
|
+
row.appendChild(el("div", { class: "bar-val" }, pct(g.solved_rate) + " (" + g.solved + "/" + g.runs + ")"));
|
|
283
|
+
host.appendChild(row);
|
|
284
|
+
});
|
|
285
|
+
})();
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------- outcomes stacked bar
|
|
288
|
+
(function () {
|
|
289
|
+
var host = document.getElementById("chart-outcomes");
|
|
290
|
+
var legend = document.getElementById("legend-outcomes");
|
|
291
|
+
var outcomeColors = { Submitted: "#3ecf8e", LimitsExceeded: "#e8b339", TimeExceeded: "#e8b339", RepeatedFormatError: "#ef5a6f", ContextWindowExceeded: "#ef5a6f", Error: "#ef5a6f", Unknown: "#8b93a7" };
|
|
292
|
+
function colorForOutcome(k) { return outcomeColors[k] || "#8b93a7"; }
|
|
293
|
+
var allOutcomes = {};
|
|
294
|
+
(DATA.leaderboard || []).forEach(function (g) {
|
|
295
|
+
var row = el("div", { class: "bar-row" });
|
|
296
|
+
row.appendChild(el("div", { class: "lbl mono" }, esc(g.slug)));
|
|
297
|
+
var track = el("div", { class: "bar-track" });
|
|
298
|
+
var total = g.runs || 1;
|
|
299
|
+
Object.keys(g.outcomes || {}).forEach(function (k) {
|
|
300
|
+
allOutcomes[k] = true;
|
|
301
|
+
var w = (g.outcomes[k] / total) * 100;
|
|
302
|
+
track.appendChild(el("div", { class: "bar-seg", style: "width:" + w + "%;background:" + colorForOutcome(k) }));
|
|
303
|
+
});
|
|
304
|
+
row.appendChild(track);
|
|
305
|
+
row.appendChild(el("div", { class: "bar-val" }, g.runs + " runs"));
|
|
306
|
+
host.appendChild(row);
|
|
307
|
+
});
|
|
308
|
+
Object.keys(allOutcomes).forEach(function (k) {
|
|
309
|
+
legend.appendChild(el("span", {}, '<span class="sw" style="background:' + colorForOutcome(k) + '"></span>' + esc(k)));
|
|
310
|
+
});
|
|
311
|
+
})();
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------- leaderboard table
|
|
314
|
+
(function () {
|
|
315
|
+
var container = document.getElementById("tbl-leaderboard");
|
|
316
|
+
var columns = [
|
|
317
|
+
{ key: "model", label: "Model", render: function (r) { return esc(r.model); } },
|
|
318
|
+
{ key: "reasoning", label: "Reasoning", render: function (r) { return esc(r.reasoning); } },
|
|
319
|
+
{ key: "runs", label: "Runs", render: function (r) { return fmt(r.runs); } },
|
|
320
|
+
{ key: "solved_rate", label: "Solved", render: function (r) { return r.solved + "/" + r.runs + " (" + pct(r.solved_rate) + ")"; } },
|
|
321
|
+
{ key: "avg_steps", label: "Avg steps", render: function (r) { return fmt1(r.avg_steps); } },
|
|
322
|
+
{ key: "avg_elapsed_seconds", label: "Avg time", render: function (r) { return fmt1(r.avg_elapsed_seconds) + "s"; } },
|
|
323
|
+
{ key: "avg_tokens", label: "Avg tokens", render: function (r) { return fmt(Math.round(r.avg_tokens)); } },
|
|
324
|
+
{ key: "cost_usd", label: "Cost", render: function (r) { return fmtUsd(r.cost_usd) + (r.cost_source === "estimated" ? "~" : ""); } },
|
|
325
|
+
{ key: "avg_n_failed_commands", label: "Fail/run", render: function (r) { return fmt1(r.avg_n_failed_commands); } },
|
|
326
|
+
];
|
|
327
|
+
sortableTable(container, columns, DATA.leaderboard || [], { defaultKey: "solved_rate" });
|
|
328
|
+
})();
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------- token mix stacked bar
|
|
331
|
+
(function () {
|
|
332
|
+
var host = document.getElementById("chart-tokens");
|
|
333
|
+
var legend = document.getElementById("legend-tokens");
|
|
334
|
+
var keys = ["prompt", "completion", "reasoning", "cached"];
|
|
335
|
+
var colors = { prompt: "#5b9dff", completion: "#3ecf8e", reasoning: "#b57bff", cached: "#8b93a7" };
|
|
336
|
+
var maxTotal = Math.max(1, Math.max.apply(null, (DATA.leaderboard || []).map(function (g) {
|
|
337
|
+
return keys.reduce(function (s, k) { return s + (g.tokens[k] || 0); }, 0);
|
|
338
|
+
})));
|
|
339
|
+
(DATA.leaderboard || []).forEach(function (g) {
|
|
340
|
+
var row = el("div", { class: "bar-row" });
|
|
341
|
+
row.appendChild(el("div", { class: "lbl mono" }, esc(g.slug)));
|
|
342
|
+
var track = el("div", { class: "bar-track" });
|
|
343
|
+
var total = keys.reduce(function (s, k) { return s + (g.tokens[k] || 0); }, 0);
|
|
344
|
+
keys.forEach(function (k) {
|
|
345
|
+
var w = (g.tokens[k] || 0) / maxTotal * 100;
|
|
346
|
+
if (w > 0) track.appendChild(el("div", { class: "bar-seg", style: "width:" + w + "%;background:" + colors[k] }));
|
|
347
|
+
});
|
|
348
|
+
row.appendChild(track);
|
|
349
|
+
row.appendChild(el("div", { class: "bar-val" }, fmt(total)));
|
|
350
|
+
host.appendChild(row);
|
|
351
|
+
});
|
|
352
|
+
keys.forEach(function (k) {
|
|
353
|
+
legend.appendChild(el("span", {}, '<span class="sw" style="background:' + colors[k] + '"></span>' + k));
|
|
354
|
+
});
|
|
355
|
+
})();
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------- scatter: cost vs solve rate
|
|
358
|
+
(function () {
|
|
359
|
+
var host = document.getElementById("chart-scatter");
|
|
360
|
+
var W = host.clientWidth || 460, H = 260, pad = { l: 42, r: 16, t: 14, b: 30 };
|
|
361
|
+
var svg = svgEl("svg", { width: "100%", height: H, viewBox: "0 0 " + W + " " + H });
|
|
362
|
+
var data = (DATA.leaderboard || []).filter(function (g) { return g.cost_usd != null; });
|
|
363
|
+
var maxCost = Math.max(0.001, Math.max.apply(null, data.map(function (g) { return g.cost_usd; }).concat([0.001])));
|
|
364
|
+
function x(v) { return pad.l + (v / maxCost) * (W - pad.l - pad.r); }
|
|
365
|
+
function y(v) { return H - pad.b - v * (H - pad.t - pad.b); }
|
|
366
|
+
// axes
|
|
367
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: W - pad.r, y1: H - pad.b, y2: H - pad.b, stroke: "#262b36" }));
|
|
368
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: pad.l, y1: pad.t, y2: H - pad.b, stroke: "#262b36" }));
|
|
369
|
+
[0, 0.25, 0.5, 0.75, 1].forEach(function (t) {
|
|
370
|
+
var ty = y(t);
|
|
371
|
+
var lab = svgEl("text", { x: pad.l - 8, y: ty + 3, "text-anchor": "end" });
|
|
372
|
+
lab.textContent = Math.round(t * 100) + "%";
|
|
373
|
+
svg.appendChild(lab);
|
|
374
|
+
});
|
|
375
|
+
var lab2 = svgEl("text", { x: W - pad.r, y: H - pad.b + 20, "text-anchor": "end" });
|
|
376
|
+
lab2.textContent = "cost \u2192 (max " + fmtUsd(maxCost) + ")";
|
|
377
|
+
svg.appendChild(lab2);
|
|
378
|
+
data.forEach(function (g) {
|
|
379
|
+
var cx = x(g.cost_usd), cy = y(g.solved_rate);
|
|
380
|
+
var r = 6 + Math.min(10, (g.avg_tokens || 0) / 4000);
|
|
381
|
+
var c = svgEl("circle", { cx: cx, cy: cy, r: r, fill: colorFor(g.slug), "fill-opacity": 0.85 });
|
|
382
|
+
svg.appendChild(c);
|
|
383
|
+
var t = svgEl("text", { x: cx + r + 4, y: cy + 3 });
|
|
384
|
+
t.textContent = g.slug;
|
|
385
|
+
svg.appendChild(t);
|
|
386
|
+
});
|
|
387
|
+
host.appendChild(svg);
|
|
388
|
+
})();
|
|
389
|
+
|
|
390
|
+
// ---------------------------------------------------------------- per-step line charts
|
|
391
|
+
function lineChart(host, series, valueFn, opts) {
|
|
392
|
+
opts = opts || {};
|
|
393
|
+
var W = host.clientWidth || 460, H = 260, pad = { l: 46, r: 16, t: 14, b: 26 };
|
|
394
|
+
var svg = svgEl("svg", { width: "100%", height: H, viewBox: "0 0 " + W + " " + H });
|
|
395
|
+
var maxStep = 1, maxVal = 0.0001;
|
|
396
|
+
series.forEach(function (s) {
|
|
397
|
+
s.points.forEach(function (p) {
|
|
398
|
+
if (p.step > maxStep) maxStep = p.step;
|
|
399
|
+
var v = valueFn(p);
|
|
400
|
+
if (v > maxVal) maxVal = v;
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
function x(v) { return pad.l + (v - 1) / Math.max(1, maxStep - 1) * (W - pad.l - pad.r); }
|
|
404
|
+
function y(v) { return H - pad.b - (v / maxVal) * (H - pad.t - pad.b); }
|
|
405
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: W - pad.r, y1: H - pad.b, y2: H - pad.b, stroke: "#262b36" }));
|
|
406
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: pad.l, y1: pad.t, y2: H - pad.b, stroke: "#262b36" }));
|
|
407
|
+
[0, 0.5, 1].forEach(function (t) {
|
|
408
|
+
var ty = y(maxVal * t);
|
|
409
|
+
var lab = svgEl("text", { x: pad.l - 6, y: ty + 3, "text-anchor": "end" });
|
|
410
|
+
lab.textContent = opts.fmt ? opts.fmt(maxVal * t) : Math.round(maxVal * t);
|
|
411
|
+
svg.appendChild(lab);
|
|
412
|
+
});
|
|
413
|
+
series.forEach(function (s) {
|
|
414
|
+
var pts = s.points.slice().sort(function (a, b) { return a.step - b.step; });
|
|
415
|
+
var d = pts.map(function (p, i) { return (i === 0 ? "M" : "L") + x(p.step) + "," + y(valueFn(p)); }).join(" ");
|
|
416
|
+
svg.appendChild(svgEl("path", { d: d, fill: "none", stroke: colorFor(s.slug), "stroke-width": 2 }));
|
|
417
|
+
pts.forEach(function (p) {
|
|
418
|
+
svg.appendChild(svgEl("circle", { cx: x(p.step), cy: y(valueFn(p)), r: 2.5, fill: colorFor(s.slug) }));
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
host.appendChild(svg);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
(function () {
|
|
425
|
+
var bySlug = {};
|
|
426
|
+
(DATA.step_series || []).forEach(function (r) {
|
|
427
|
+
if (!bySlug[r.slug]) bySlug[r.slug] = [];
|
|
428
|
+
bySlug[r.slug].push(r);
|
|
429
|
+
});
|
|
430
|
+
var series = Object.keys(bySlug).map(function (slug) { return { slug: slug, points: bySlug[slug] }; });
|
|
431
|
+
if (series.length === 0) {
|
|
432
|
+
document.getElementById("section-series").style.display = "none";
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
lineChart(document.getElementById("chart-line-tokens"), series, function (p) { return p.prompt_tokens.p50; }, { fmt: fmt });
|
|
436
|
+
lineChart(document.getElementById("chart-line-cost"), series, function (p) { return p.cum_cost_usd.mean; }, { fmt: fmtUsd });
|
|
437
|
+
var legend = document.getElementById("legend-series");
|
|
438
|
+
series.forEach(function (s) {
|
|
439
|
+
legend.appendChild(el("span", {}, '<span class="sw" style="background:' + colorFor(s.slug) + '"></span>' + esc(s.slug)));
|
|
440
|
+
});
|
|
441
|
+
})();
|
|
442
|
+
|
|
443
|
+
// ---------------------------------------------------------------- model x task matrix (heatmap table)
|
|
444
|
+
(function () {
|
|
445
|
+
var matrix = DATA.matrix || [];
|
|
446
|
+
var container = document.getElementById("tbl-matrix");
|
|
447
|
+
if (matrix.length === 0) {
|
|
448
|
+
document.getElementById("section-matrix").style.display = "none";
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
var tasks = [];
|
|
452
|
+
var slugs = [];
|
|
453
|
+
var cell = {};
|
|
454
|
+
matrix.forEach(function (m) {
|
|
455
|
+
if (tasks.indexOf(m.task_id) === -1) tasks.push(m.task_id);
|
|
456
|
+
if (slugs.indexOf(m.slug) === -1) slugs.push(m.slug);
|
|
457
|
+
cell[m.slug + "\u0000" + m.task_id] = m;
|
|
458
|
+
});
|
|
459
|
+
function heatColor(rate) {
|
|
460
|
+
if (rate == null) return "transparent";
|
|
461
|
+
var g = Math.round(60 + rate * 140);
|
|
462
|
+
var r = Math.round(220 - rate * 160);
|
|
463
|
+
return "rgba(" + r + "," + g + ",120,0.35)";
|
|
464
|
+
}
|
|
465
|
+
var thead = "<thead><tr><th>Model</th>" + tasks.map(function (t) { return "<th>" + esc(t) + "</th>"; }).join("") + "</tr></thead>";
|
|
466
|
+
var tbody = "<tbody>" + slugs.map(function (slug) {
|
|
467
|
+
var cells = tasks.map(function (t) {
|
|
468
|
+
var m = cell[slug + "\u0000" + t];
|
|
469
|
+
if (!m) return "<td>—</td>";
|
|
470
|
+
return '<td><span class="heat-cell" style="background:' + heatColor(m.pass_at_k) + '">' + m.passed + "/" + m.k + "<br>" + pct(m.pass_at_k) + "</span></td>";
|
|
471
|
+
}).join("");
|
|
472
|
+
return "<tr><td class=\"mono\">" + esc(slug) + "</td>" + cells + "</tr>";
|
|
473
|
+
}).join("") + "</tbody>";
|
|
474
|
+
container.innerHTML = thead + tbody;
|
|
475
|
+
})();
|
|
476
|
+
|
|
477
|
+
// ---------------------------------------------------------------- runs table + filters + expandable timeline
|
|
478
|
+
(function () {
|
|
479
|
+
var runs = DATA.runs || [];
|
|
480
|
+
var container = document.getElementById("tbl-runs");
|
|
481
|
+
var fModel = document.getElementById("filter-model");
|
|
482
|
+
var fTask = document.getElementById("filter-task");
|
|
483
|
+
var fOutcome = document.getElementById("filter-outcome");
|
|
484
|
+
|
|
485
|
+
function uniq(fn) {
|
|
486
|
+
var seen = {}, out = [];
|
|
487
|
+
runs.forEach(function (r) { var v = fn(r); if (v && !seen[v]) { seen[v] = 1; out.push(v); } });
|
|
488
|
+
return out.sort();
|
|
489
|
+
}
|
|
490
|
+
uniq(function (r) { return r.slug; }).forEach(function (v) { fModel.appendChild(el("option", { value: v }, esc(v))); });
|
|
491
|
+
uniq(function (r) { return r.task_id; }).forEach(function (v) { fTask.appendChild(el("option", { value: v }, esc(v))); });
|
|
492
|
+
uniq(function (r) { return r.exit_status; }).forEach(function (v) { fOutcome.appendChild(el("option", { value: v }, esc(v))); });
|
|
493
|
+
|
|
494
|
+
var columns = [
|
|
495
|
+
{ key: "slug", label: "Model", render: function (r) { return '<span class="mono">' + esc(r.slug) + "</span>"; } },
|
|
496
|
+
{ key: "task_id", label: "Task", render: function (r) { return esc(r.task_id); } },
|
|
497
|
+
{ key: "run_id", label: "Run", render: function (r) { return esc(r.run_id); } },
|
|
498
|
+
{ key: "exit_status", label: "Outcome", render: function (r) {
|
|
499
|
+
var cls = r.resolved ? "ok" : (r.exit_status === "Error" ? "bad" : "warn");
|
|
500
|
+
return '<span class="pill ' + cls + '">' + esc(r.exit_status || "Unknown") + "</span>";
|
|
501
|
+
} },
|
|
502
|
+
{ key: "steps", label: "Steps", render: function (r) { return fmt(r.steps); } },
|
|
503
|
+
{ key: "elapsed_seconds", label: "Time", render: function (r) { return fmt(r.elapsed_seconds) + "s"; } },
|
|
504
|
+
{ key: "tokens", label: "Tokens", render: function (r) { return fmt(r.tokens.total); } },
|
|
505
|
+
{ key: "cost_usd", label: "Cost", render: function (r) { return fmtUsd(r.cost_usd) + (r.cost_source === "estimated" ? "~" : ""); } },
|
|
506
|
+
{ key: "n_failed_commands", label: "Fails", render: function (r) { return fmt(r.n_failed_commands); } },
|
|
507
|
+
];
|
|
508
|
+
|
|
509
|
+
function renderTimeline(run) {
|
|
510
|
+
var box = el("div", { class: "timeline" });
|
|
511
|
+
(run.timeline || []).forEach(function (s) {
|
|
512
|
+
var rcClass = s.returncode === 0 ? "rc0" : (s.returncode == null ? "muted" : "rcN");
|
|
513
|
+
var step = el("div", { class: "step" });
|
|
514
|
+
step.innerHTML =
|
|
515
|
+
'<div class="head"><span class="n">step ' + s.step + '</span>' +
|
|
516
|
+
'<span class="stats">' +
|
|
517
|
+
fmt(s.tokens.total) + " tok" +
|
|
518
|
+
(s.cost_usd != null ? " \u00b7 " + fmtUsd(s.cost_usd) : "") +
|
|
519
|
+
(s.returncode != null ? ' \u00b7 rc=<span class="' + rcClass + '">' + s.returncode + "</span>" : "") +
|
|
520
|
+
"</span></div>" +
|
|
521
|
+
(s.thought ? '<div class="thought">' + esc(s.thought) + "</div>" : "") +
|
|
522
|
+
(s.command ? "<pre>$ " + esc(s.command) + "</pre>" : "") +
|
|
523
|
+
(s.output_preview ? "<pre>" + esc(s.output_preview) + "</pre>" : "");
|
|
524
|
+
box.appendChild(step);
|
|
525
|
+
});
|
|
526
|
+
return box;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function draw() {
|
|
530
|
+
var filtered = runs.filter(function (r) {
|
|
531
|
+
if (fModel.value && r.slug !== fModel.value) return false;
|
|
532
|
+
if (fTask.value && r.task_id !== fTask.value) return false;
|
|
533
|
+
if (fOutcome.value && r.exit_status !== fOutcome.value) return false;
|
|
534
|
+
return true;
|
|
535
|
+
});
|
|
536
|
+
sortableTable(container, columns, filtered, {
|
|
537
|
+
defaultKey: "slug",
|
|
538
|
+
defaultDir: 1,
|
|
539
|
+
rowAttrs: function (r, i) { return 'class="run-row" data-idx="' + i + '"'; },
|
|
540
|
+
afterDraw: function (containerEl, sorted) {
|
|
541
|
+
containerEl.querySelectorAll("tbody tr").forEach(function (tr) {
|
|
542
|
+
var idx = Number(tr.getAttribute("data-idx"));
|
|
543
|
+
var run = sorted[idx];
|
|
544
|
+
tr.addEventListener("click", function () {
|
|
545
|
+
var next = tr.nextElementSibling;
|
|
546
|
+
if (next && next.classList.contains("detail-row")) {
|
|
547
|
+
next.remove();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
containerEl.querySelectorAll(".detail-row").forEach(function (d) { d.remove(); });
|
|
551
|
+
var detailRow = document.createElement("tr");
|
|
552
|
+
detailRow.className = "detail-row";
|
|
553
|
+
var td = document.createElement("td");
|
|
554
|
+
td.colSpan = columns.length;
|
|
555
|
+
td.appendChild(renderTimeline(run));
|
|
556
|
+
detailRow.appendChild(td);
|
|
557
|
+
tr.parentNode.insertBefore(detailRow, tr.nextSibling);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
[fModel, fTask, fOutcome].forEach(function (elm) { elm.addEventListener("change", draw); });
|
|
564
|
+
draw();
|
|
565
|
+
})();
|
|
566
|
+
})();
|
|
567
|
+
</script>
|
|
568
|
+
</body>
|
|
569
|
+
</html>
|