@hackerrank/astra-cli 0.1.2 → 0.1.3
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 +10 -2
- package/package.json +1 -1
- package/src/bench.js +26 -5
- package/src/cli.js +33 -2
- package/src/report.html +116 -105
- package/src/report.js +3 -1
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ astra \
|
|
|
115
115
|
--repeat 3 \
|
|
116
116
|
--tar # write a bench/ tarball locally
|
|
117
117
|
|
|
118
|
-
# push results to S3 (tars bench/
|
|
118
|
+
# push results to S3 (tars bench/ and uploads it + report.html to the URI)
|
|
119
119
|
astra -m m1,m2 -p tasks/dummy-slugify --repeat 3 \
|
|
120
120
|
--push s3://hackerrank-astra-bench-results-dev/runs/2025-09-02/
|
|
121
121
|
```
|
|
@@ -126,6 +126,12 @@ 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
|
+
Next to the tarball, `--push` also uploads the self-contained `report.html`
|
|
130
|
+
dashboard with `Content-Type: text/html` and prints a **7-day presigned
|
|
131
|
+
"view report" link** at the end of the run. The bucket is private (no public
|
|
132
|
+
read), so that presigned URL is the only way to open the dashboard — e.g.
|
|
133
|
+
clickable straight from a Jenkins console log.
|
|
134
|
+
|
|
129
135
|
### HTML report
|
|
130
136
|
|
|
131
137
|
Every matrix run (and every single `-p`/`-t`/`-f` bench run) rebuilds
|
|
@@ -175,7 +181,9 @@ credits — that's a quota issue, not a bug.
|
|
|
175
181
|
--repeat <n> Attempts per (model,reasoning) in a matrix (default: 1)
|
|
176
182
|
--bench-root <dir> Root folder for bench runs (default: ./bench)
|
|
177
183
|
--tar After a matrix, write a bench/ tarball locally
|
|
178
|
-
--push <s3-uri> After a matrix, tar bench/ and `aws s3 cp`
|
|
184
|
+
--push <s3-uri> After a matrix, tar bench/ and `aws s3 cp` it (plus
|
|
185
|
+
report.html) to the URI; prints presigned
|
|
186
|
+
download/view links
|
|
179
187
|
--report Rebuild bench/summary.json + bench/report.html from
|
|
180
188
|
whatever runs already exist on disk, then exit
|
|
181
189
|
-C, --cwd <path> Working directory for commands (default: cwd)
|
package/package.json
CHANGED
package/src/bench.js
CHANGED
|
@@ -455,17 +455,38 @@ function s3ConsoleUrl(s3Uri) {
|
|
|
455
455
|
return `https://s3.console.aws.amazon.com/s3/object/${bucket}?prefix=${encodeURIComponent(key)}`;
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Generate a time-limited presigned URL for an S3 object via the AWS CLI
|
|
460
|
+
* (`aws s3 presign`). `expiresIn` defaults to 7 days (604800s). Returns null
|
|
461
|
+
* (rather than throwing) if the AWS CLI is missing or presigning fails, so
|
|
462
|
+
* a presign failure never blocks the run.
|
|
463
|
+
*/
|
|
464
|
+
export function presignS3Url(s3Uri, expiresIn = 604800) {
|
|
465
|
+
const res = spawnSync(
|
|
466
|
+
"aws",
|
|
467
|
+
["s3", "presign", s3Uri, "--expires-in", String(expiresIn)],
|
|
468
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
469
|
+
);
|
|
470
|
+
if (res.error && res.error.code === "ENOENT") return null;
|
|
471
|
+
if (res.status !== 0) return null;
|
|
472
|
+
return (res.stdout?.toString() || "").trim() || null;
|
|
473
|
+
}
|
|
474
|
+
|
|
458
475
|
/**
|
|
459
476
|
* Upload a file to S3 via the AWS CLI. Returns { uri, url } where `uri` is the
|
|
460
477
|
* final s3:// destination and `url` is a browsable https console link.
|
|
461
|
-
*
|
|
462
|
-
*
|
|
478
|
+
* Optionally sets the object's Content-Type (e.g. "text/html" so presigned
|
|
479
|
+
* URLs open in a browser instead of downloading). Fails gracefully (throws a
|
|
480
|
+
* descriptive error) when the AWS CLI is missing or the copy fails; callers
|
|
481
|
+
* should keep the local tarball on failure.
|
|
463
482
|
*/
|
|
464
|
-
export function pushToS3(file, s3Uri) {
|
|
483
|
+
export function pushToS3(file, s3Uri, { contentType } = {}) {
|
|
465
484
|
if (!/^s3:\/\//.test(s3Uri)) throw new Error(`--push destination must be an s3:// URI, got: ${s3Uri}`);
|
|
466
485
|
// If the URI ends with "/", upload into it under the file's basename.
|
|
467
486
|
const dest = s3Uri.endsWith("/") ? s3Uri + path.basename(file) : s3Uri;
|
|
468
|
-
const
|
|
487
|
+
const cmd = ["s3", "cp", file, dest];
|
|
488
|
+
if (contentType) cmd.push("--content-type", contentType);
|
|
489
|
+
const res = spawnSync("aws", cmd, {
|
|
469
490
|
stdio: ["ignore", "pipe", "pipe"],
|
|
470
491
|
});
|
|
471
492
|
if (res.error && res.error.code === "ENOENT") {
|
|
@@ -474,5 +495,5 @@ export function pushToS3(file, s3Uri) {
|
|
|
474
495
|
if (res.status !== 0) {
|
|
475
496
|
throw new Error(`aws s3 cp failed: ${res.stderr?.toString() || "unknown"}`);
|
|
476
497
|
}
|
|
477
|
-
return { uri: dest, url: s3ConsoleUrl(dest) };
|
|
498
|
+
return { uri: dest, url: s3ConsoleUrl(dest), presigned: presignS3Url(dest) };
|
|
478
499
|
}
|
package/src/cli.js
CHANGED
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
* comma-separated to sweep levels in bench mode).
|
|
29
29
|
* Sent to the model and recorded in the bench name.
|
|
30
30
|
* --repeat <n> Attempts per (model,reasoning) in a matrix (1)
|
|
31
|
-
* --push After a matrix, tar bench/ and upload
|
|
32
|
-
* s3://astra-bench-results/<timestamp
|
|
31
|
+
* --push After a matrix, tar bench/ and upload it (plus
|
|
32
|
+
* report.html) to s3://astra-bench-results/<timestamp>/;
|
|
33
|
+
* prints presigned download/view links
|
|
33
34
|
* --push-uri <s3-uri> Override the S3 push destination
|
|
34
35
|
* --tar After a matrix, write a bench/ tarball locally
|
|
35
36
|
* --report Rebuild bench/summary.json + bench/report.html
|
|
@@ -365,6 +366,12 @@ async function main() {
|
|
|
365
366
|
const metrics = collectMetrics({ agent: benchAgent, model, reasoning });
|
|
366
367
|
const paths = writeMetrics({ runDir: alloc.dir, root: alloc.root, metrics });
|
|
367
368
|
log(`[astra] bench done: exit=${res.exit_status} · metrics → ${paths.runMetrics}`);
|
|
369
|
+
try {
|
|
370
|
+
const report = refreshReport(alloc.root);
|
|
371
|
+
log(`[astra] report → ${report.html}`);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
log(`[astra] report generation skipped: ${err.message}`);
|
|
374
|
+
}
|
|
368
375
|
};
|
|
369
376
|
await runRepl(agent, { model, autoRun: !!args.yolo, fresh: !resumeDoc, reasoning, runBench });
|
|
370
377
|
process.exit(0);
|
|
@@ -465,6 +472,30 @@ function archiveAndPush(args, root, links) {
|
|
|
465
472
|
const pushed = pushToS3(tarball, dest);
|
|
466
473
|
links.push(["s3 uri", pushed.uri]);
|
|
467
474
|
links.push(["s3 url", pushed.url]);
|
|
475
|
+
if (pushed.presigned) {
|
|
476
|
+
links.push(["download (presigned, 7d)", pushed.presigned]);
|
|
477
|
+
}
|
|
478
|
+
// Also push the self-contained HTML dashboard next to the tarball and
|
|
479
|
+
// emit a presigned view link: the bucket is private (no public read),
|
|
480
|
+
// so the presigned URL is the only way to open the report — e.g. by
|
|
481
|
+
// clicking it straight from the Jenkins console output. Uploaded with
|
|
482
|
+
// Content-Type: text/html so browsers render it instead of downloading.
|
|
483
|
+
const reportHtml = path.join(benchRoot(root), "report.html");
|
|
484
|
+
if (fs.existsSync(reportHtml)) {
|
|
485
|
+
const htmlDest = dest.endsWith("/")
|
|
486
|
+
? dest + "report.html"
|
|
487
|
+
: dest.slice(0, dest.lastIndexOf("/") + 1) + "report.html";
|
|
488
|
+
try {
|
|
489
|
+
const html = pushToS3(reportHtml, htmlDest, { contentType: "text/html" });
|
|
490
|
+
if (html.presigned) {
|
|
491
|
+
links.push(["view report (presigned, 7d)", html.presigned]);
|
|
492
|
+
} else {
|
|
493
|
+
links.push(["report (s3 url)", html.url]);
|
|
494
|
+
}
|
|
495
|
+
} catch (err) {
|
|
496
|
+
console.error(`\x1b[31m[astra] S3 report push failed: ${err.message}\x1b[0m`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
468
499
|
} catch (err) {
|
|
469
500
|
console.error(`\x1b[31m[astra] S3 push failed: ${err.message}\x1b[0m`);
|
|
470
501
|
console.error(`\x1b[33m[astra] tarball kept locally: ${tarball}\x1b[0m`);
|
package/src/report.html
CHANGED
|
@@ -4,41 +4,15 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>astra bench report</title>
|
|
7
|
+
<link rel="stylesheet" href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&display=swap" />
|
|
7
8
|
<style>
|
|
8
|
-
:root {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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; }
|
|
9
|
+
:root { --ink:#11131a; --muted:#69707d; --wash:#f5f6f8; --line:#e4e7eb; --lime:#a7ff63; --blue:#5285f7; --purple:#9160d8; --green:#39ae76; --orange:#f3a021; --red:#d85668; font-family:Satoshi,"Avenir Next",Arial,sans-serif; }
|
|
10
|
+
* { box-sizing:border-box; } body { margin:0; background:#fff; color:var(--ink); font-size:14px; line-height:1.45; } .wrap { max-width:1240px; margin:auto; padding:28px 32px 68px; }
|
|
11
|
+
header.top { display:flex; justify-content:space-between; gap:20px; align-items:flex-start; padding-bottom:28px; border-bottom:1px solid var(--line); } header.top h1 { font-size:32px; letter-spacing:-.045em; line-height:1; margin:8px 0 0; font-weight:700; } .eyebrow { display:inline-block; padding:5px 14px; border-radius:99px; background:var(--lime); font-size:12px; font-weight:700; } header.top .meta { color:var(--muted); font-size:12px; text-align:right; }
|
|
12
|
+
.kpis { display:grid; grid-template-columns:repeat(4,1fr); margin:24px 0 52px; } .kpi { min-height:96px; padding:8px 18px; border-left:1px solid var(--line); } .kpi:first-child { border:0; padding-left:0; } .kpi .v { font-size:28px; letter-spacing:-.045em; font-weight:700; } .kpi .l { color:var(--muted); font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; margin-top:8px; }
|
|
13
|
+
section { margin:56px 0; } section > h2 { font-size:20px; letter-spacing:-.035em; margin:0 0 22px; } section > h2 .sub { color:var(--muted); font-size:13px; font-weight:400; letter-spacing:0; } .grid2 { display:grid; grid-template-columns:1.1fr 1fr; gap:44px; } .panel { padding:0; } .panel h3 { color:var(--muted); letter-spacing:.07em; text-transform:uppercase; font-size:12px; margin:0 0 16px; }
|
|
40
14
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
41
|
-
th, td { text-align: left; padding:
|
|
15
|
+
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
|
|
42
16
|
th { color: var(--muted); font-weight: 600; font-size: 11.5px; text-transform: uppercase; letter-spacing: .03em; cursor: pointer; user-select: none; }
|
|
43
17
|
th:hover { color: var(--text); }
|
|
44
18
|
th .arrow { opacity: .5; font-size: 10px; margin-left: 3px; }
|
|
@@ -52,7 +26,7 @@
|
|
|
52
26
|
.muted { color: var(--muted); }
|
|
53
27
|
.bar-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; }
|
|
54
28
|
.bar-row .lbl { width: 150px; flex: 0 0 150px; font-size: 12px; overflow: hidden; text-overflow: ellipsis; }
|
|
55
|
-
.bar-track { flex: 1; height:
|
|
29
|
+
.bar-track { flex: 1; height: 9px; background: var(--wash); border-radius: 99px; overflow: hidden; display: flex; }
|
|
56
30
|
.bar-seg { height: 100%; }
|
|
57
31
|
.bar-val { width: 64px; flex: 0 0 64px; text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
58
32
|
.legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 10px; font-size: 11.5px; color: var(--muted); }
|
|
@@ -61,7 +35,7 @@
|
|
|
61
35
|
.heat-cell { display: inline-block; min-width: 44px; padding: 3px 6px; border-radius: 5px; font-size: 12px; }
|
|
62
36
|
details.run-detail { margin: 0; }
|
|
63
37
|
details.run-detail > summary { display: none; }
|
|
64
|
-
.timeline { margin: 0 0 18px;
|
|
38
|
+
.timeline { margin: 0 0 18px; background:var(--wash); padding:0 14px; overflow: hidden; }
|
|
65
39
|
.timeline .step { padding: 10px 14px; border-bottom: 1px solid var(--border); }
|
|
66
40
|
.timeline .step:last-child { border-bottom: none; }
|
|
67
41
|
.timeline .step .head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; margin-bottom: 4px; }
|
|
@@ -71,35 +45,30 @@
|
|
|
71
45
|
.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
46
|
.rc0 { color: var(--good); } .rcN { color: var(--bad); }
|
|
73
47
|
.toolbar { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
|
|
74
|
-
.toolbar select, .toolbar input { background: var(--
|
|
48
|
+
.toolbar select, .toolbar input { background: var(--wash); border:0; color:var(--ink); border-radius:4px; padding:7px 9px; font-size:12px; } .model-filter { align-items:end; } .model-filter > span { color:var(--muted); font-size:11px; } .model-menu { position:relative; } .model-menu summary { cursor:pointer; list-style:none; min-width:220px; padding:9px 32px 9px 11px; background:var(--wash); border-radius:5px; font-size:12px; position:relative; } .model-menu summary::-webkit-details-marker { display:none; } .model-menu summary::after { content:"⌄"; position:absolute; right:11px; color:var(--muted); } .model-options { position:absolute; z-index:2; top:calc(100% + 6px); left:0; width:260px; max-height:240px; overflow:auto; padding:6px; background:#fff; border:1px solid var(--line); box-shadow:0 10px 24px rgba(17,19,26,.1); } .model-option { display:flex; gap:9px; align-items:center; padding:8px; font-size:12px; cursor:pointer; } .model-option:hover { background:var(--wash); } .model-option input { accent-color:var(--blue); }
|
|
75
49
|
.toolbar label { color: var(--muted); font-size: 12px; }
|
|
76
50
|
footer { color: var(--muted); font-size: 11.5px; margin-top: 40px; text-align: center; }
|
|
77
51
|
svg text { fill: var(--muted); font-size: 10.5px; }
|
|
78
|
-
.axis line, .axis path { stroke: var(--
|
|
52
|
+
.axis line, .axis path { stroke: var(--line); }
|
|
79
53
|
.n-a { color: var(--muted); }
|
|
80
54
|
</style>
|
|
81
55
|
</head>
|
|
82
56
|
<body>
|
|
83
57
|
<div class="wrap">
|
|
84
58
|
<header class="top">
|
|
85
|
-
<h1>
|
|
86
|
-
<div class="meta" id="generated"></div>
|
|
59
|
+
<div><h1>Benchmark report</h1><div class="meta" id="generated"></div></div>
|
|
87
60
|
</header>
|
|
88
61
|
|
|
89
62
|
<div class="kpis" id="kpis"></div>
|
|
90
63
|
|
|
91
64
|
<section>
|
|
92
|
-
<h2>
|
|
93
|
-
<div class="
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<h3>Outcomes</h3>
|
|
100
|
-
<div id="chart-outcomes"></div>
|
|
101
|
-
<div class="legend" id="legend-outcomes"></div>
|
|
102
|
-
</div>
|
|
65
|
+
<h2>Performance <span class="sub">run volume and reliability by model</span></h2>
|
|
66
|
+
<div class="toolbar model-filter">
|
|
67
|
+
<span>Models</span>
|
|
68
|
+
<details class="model-menu">
|
|
69
|
+
<summary id="model-filter-label">All models</summary>
|
|
70
|
+
<div id="leaderboard-model-options" class="model-options"></div>
|
|
71
|
+
</details>
|
|
103
72
|
</div>
|
|
104
73
|
<div class="panel" style="margin-top:16px;">
|
|
105
74
|
<table id="tbl-leaderboard"></table>
|
|
@@ -107,22 +76,29 @@
|
|
|
107
76
|
</section>
|
|
108
77
|
|
|
109
78
|
<section>
|
|
110
|
-
<h2>
|
|
79
|
+
<h2>Efficiency <span class="sub">spend and token composition</span></h2>
|
|
111
80
|
<div class="grid2">
|
|
112
81
|
<div class="panel">
|
|
113
|
-
<h3>Cost vs.
|
|
82
|
+
<h3>Cost vs. average duration</h3>
|
|
114
83
|
<div id="chart-scatter"></div>
|
|
115
84
|
</div>
|
|
116
85
|
<div class="panel">
|
|
117
|
-
<h3>
|
|
118
|
-
<div id="chart-
|
|
119
|
-
<div class="legend" id="legend-tokens"></div>
|
|
86
|
+
<h3>Cost vs. average steps</h3>
|
|
87
|
+
<div id="chart-scatter-steps"></div>
|
|
120
88
|
</div>
|
|
121
89
|
</div>
|
|
122
90
|
</section>
|
|
123
91
|
|
|
92
|
+
<section>
|
|
93
|
+
<h2>Token mix <span class="sub">prompt, completion, reasoning, and cached tokens</span></h2>
|
|
94
|
+
<div class="panel">
|
|
95
|
+
<div id="chart-tokens"></div>
|
|
96
|
+
<div class="legend" id="legend-tokens"></div>
|
|
97
|
+
</div>
|
|
98
|
+
</section>
|
|
99
|
+
|
|
124
100
|
<section id="section-series">
|
|
125
|
-
<h2>
|
|
101
|
+
<h2>Trajectory <span class="sub">where context and cost accumulate</span></h2>
|
|
126
102
|
<div class="grid2">
|
|
127
103
|
<div class="panel">
|
|
128
104
|
<h3>Prompt tokens (context) vs. step</h3>
|
|
@@ -136,31 +112,6 @@
|
|
|
136
112
|
<div class="legend" id="legend-series"></div>
|
|
137
113
|
</section>
|
|
138
114
|
|
|
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
115
|
<footer>generated by <span class="mono">astra bench --report</span> · schema <span id="schema"></span></footer>
|
|
165
116
|
</div>
|
|
166
117
|
|
|
@@ -169,8 +120,19 @@
|
|
|
169
120
|
</script>
|
|
170
121
|
<script>
|
|
171
122
|
(function () {
|
|
172
|
-
|
|
173
|
-
|
|
123
|
+
function loadReportData() {
|
|
124
|
+
try {
|
|
125
|
+
var request = new XMLHttpRequest();
|
|
126
|
+
request.open("GET", "summary.json", false);
|
|
127
|
+
request.send(null);
|
|
128
|
+
if ((request.status >= 200 && request.status < 300) || request.status === 0) return JSON.parse(request.responseText);
|
|
129
|
+
} catch (_) {
|
|
130
|
+
// Direct file access can block JSON reads; use the embedded snapshot then.
|
|
131
|
+
}
|
|
132
|
+
return window.__ASTRA_EMBEDDED_DATA__ || { kpis: {}, leaderboard: [], matrix: [], runs: [], step_series: [] };
|
|
133
|
+
}
|
|
134
|
+
var DATA = loadReportData();
|
|
135
|
+
var PALETTE = ["#5285f7", "#9160d8", "#39ae76", "#f3a021", "#d85668", "#37a8ad"];
|
|
174
136
|
var colorCache = {};
|
|
175
137
|
function colorFor(key) {
|
|
176
138
|
if (!colorCache[key]) {
|
|
@@ -209,21 +171,19 @@
|
|
|
209
171
|
}
|
|
210
172
|
|
|
211
173
|
// ---------------------------------------------------------------- header
|
|
212
|
-
document.getElementById("generated").textContent =
|
|
174
|
+
document.getElementById("generated").textContent = DATA.generated_at
|
|
175
|
+
? "Generated " + new Date(DATA.generated_at).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" })
|
|
176
|
+
: "";
|
|
213
177
|
document.getElementById("schema").textContent = DATA.schema || "";
|
|
214
178
|
|
|
215
179
|
// ---------------------------------------------------------------- KPIs
|
|
216
180
|
(function renderKpis() {
|
|
217
181
|
var k = DATA.kpis || {};
|
|
218
182
|
var cards = [
|
|
219
|
-
["
|
|
220
|
-
["
|
|
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)],
|
|
183
|
+
["Models Benchmarked", fmt(k.models)],
|
|
184
|
+
["Total duration", fmt(k.elapsed_seconds) + "s"],
|
|
225
185
|
["Total cost", fmtUsd(k.cost_usd) + (k.cost_source === "estimated" ? "~" : "")],
|
|
226
|
-
["
|
|
186
|
+
["Tokens used", fmt(k.tokens)],
|
|
227
187
|
];
|
|
228
188
|
var host = document.getElementById("kpis");
|
|
229
189
|
cards.forEach(function (c) {
|
|
@@ -266,20 +226,22 @@
|
|
|
266
226
|
draw();
|
|
267
227
|
}
|
|
268
228
|
|
|
269
|
-
// ---------------------------------------------------------------- leaderboard chart:
|
|
229
|
+
// ---------------------------------------------------------------- leaderboard chart: run volume bars
|
|
270
230
|
(function () {
|
|
271
231
|
var host = document.getElementById("chart-solve");
|
|
232
|
+
if (!host) return;
|
|
233
|
+
var maxRuns = Math.max(1, Math.max.apply(null, (DATA.leaderboard || []).map(function (g) { return g.runs || 0; })));
|
|
272
234
|
(DATA.leaderboard || []).forEach(function (g) {
|
|
273
235
|
var row = el("div", { class: "bar-row" });
|
|
274
236
|
row.appendChild(el("div", { class: "lbl mono" }, esc(g.slug)));
|
|
275
237
|
var track = el("div", { class: "bar-track" });
|
|
276
238
|
var seg = el("div", {
|
|
277
239
|
class: "bar-seg",
|
|
278
|
-
style: "width:" + Math.round((g.
|
|
240
|
+
style: "width:" + Math.round((g.runs || 0) / maxRuns * 100) + "%;background:" + colorFor(g.slug),
|
|
279
241
|
});
|
|
280
242
|
track.appendChild(seg);
|
|
281
243
|
row.appendChild(track);
|
|
282
|
-
row.appendChild(el("div", { class: "bar-val" },
|
|
244
|
+
row.appendChild(el("div", { class: "bar-val" }, fmt(g.runs) + " runs"));
|
|
283
245
|
host.appendChild(row);
|
|
284
246
|
});
|
|
285
247
|
})();
|
|
@@ -288,6 +250,7 @@
|
|
|
288
250
|
(function () {
|
|
289
251
|
var host = document.getElementById("chart-outcomes");
|
|
290
252
|
var legend = document.getElementById("legend-outcomes");
|
|
253
|
+
if (!host || !legend) return;
|
|
291
254
|
var outcomeColors = { Submitted: "#3ecf8e", LimitsExceeded: "#e8b339", TimeExceeded: "#e8b339", RepeatedFormatError: "#ef5a6f", ContextWindowExceeded: "#ef5a6f", Error: "#ef5a6f", Unknown: "#8b93a7" };
|
|
292
255
|
function colorForOutcome(k) { return outcomeColors[k] || "#8b93a7"; }
|
|
293
256
|
var allOutcomes = {};
|
|
@@ -313,18 +276,31 @@
|
|
|
313
276
|
// ---------------------------------------------------------------- leaderboard table
|
|
314
277
|
(function () {
|
|
315
278
|
var container = document.getElementById("tbl-leaderboard");
|
|
279
|
+
var options = document.getElementById("leaderboard-model-options");
|
|
280
|
+
var filterLabel = document.getElementById("model-filter-label");
|
|
316
281
|
var columns = [
|
|
317
282
|
{ key: "model", label: "Model", render: function (r) { return esc(r.model); } },
|
|
318
283
|
{ key: "reasoning", label: "Reasoning", render: function (r) { return esc(r.reasoning); } },
|
|
319
284
|
{ key: "runs", label: "Runs", render: function (r) { return fmt(r.runs); } },
|
|
320
|
-
{ key: "
|
|
321
|
-
{ key: "
|
|
322
|
-
{ key: "
|
|
323
|
-
{ key: "avg_tokens", label: "Avg tokens", render: function (r) { return fmt(Math.round(r.avg_tokens)); } },
|
|
285
|
+
{ key: "sum_steps", label: "Total steps", render: function (r) { return fmt(r.sum_steps); } },
|
|
286
|
+
{ key: "sum_elapsed_seconds", label: "Time taken", render: function (r) { return fmt(r.sum_elapsed_seconds) + "s"; } },
|
|
287
|
+
{ key: "sum_tokens", label: "Tokens", render: function (r) { return fmt(r.sum_tokens); } },
|
|
324
288
|
{ 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
289
|
];
|
|
327
|
-
|
|
290
|
+
(DATA.leaderboard || []).forEach(function (r) {
|
|
291
|
+
var option = el("label", { class: "model-option" });
|
|
292
|
+
option.appendChild(el("input", { type: "checkbox", value: r.slug }));
|
|
293
|
+
option.appendChild(document.createTextNode(r.model + " · " + r.reasoning));
|
|
294
|
+
options.appendChild(option);
|
|
295
|
+
});
|
|
296
|
+
function draw() {
|
|
297
|
+
var selected = Array.prototype.slice.call(options.querySelectorAll("input:checked")).map(function (o) { return o.value; });
|
|
298
|
+
var rows = selected.length ? (DATA.leaderboard || []).filter(function (r) { return selected.indexOf(r.slug) !== -1; }) : DATA.leaderboard || [];
|
|
299
|
+
filterLabel.textContent = selected.length ? selected.length + " model" + (selected.length === 1 ? "" : "s") + " selected" : "All models";
|
|
300
|
+
sortableTable(container, columns, rows, { defaultKey: "sum_tokens" });
|
|
301
|
+
}
|
|
302
|
+
options.addEventListener("change", draw);
|
|
303
|
+
draw();
|
|
328
304
|
})();
|
|
329
305
|
|
|
330
306
|
// ---------------------------------------------------------------- token mix stacked bar
|
|
@@ -354,29 +330,30 @@
|
|
|
354
330
|
});
|
|
355
331
|
})();
|
|
356
332
|
|
|
357
|
-
// ---------------------------------------------------------------- scatter: cost vs
|
|
333
|
+
// ---------------------------------------------------------------- scatter: cost vs duration
|
|
358
334
|
(function () {
|
|
359
335
|
var host = document.getElementById("chart-scatter");
|
|
360
336
|
var W = host.clientWidth || 460, H = 260, pad = { l: 42, r: 16, t: 14, b: 30 };
|
|
361
337
|
var svg = svgEl("svg", { width: "100%", height: H, viewBox: "0 0 " + W + " " + H });
|
|
362
338
|
var data = (DATA.leaderboard || []).filter(function (g) { return g.cost_usd != null; });
|
|
363
339
|
var maxCost = Math.max(0.001, Math.max.apply(null, data.map(function (g) { return g.cost_usd; }).concat([0.001])));
|
|
340
|
+
var maxDuration = Math.max(1, Math.max.apply(null, data.map(function (g) { return g.avg_elapsed_seconds || 0; }).concat([1])));
|
|
364
341
|
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); }
|
|
342
|
+
function y(v) { return H - pad.b - (v / maxDuration) * (H - pad.t - pad.b); }
|
|
366
343
|
// axes
|
|
367
344
|
svg.appendChild(svgEl("line", { x1: pad.l, x2: W - pad.r, y1: H - pad.b, y2: H - pad.b, stroke: "#262b36" }));
|
|
368
345
|
svg.appendChild(svgEl("line", { x1: pad.l, x2: pad.l, y1: pad.t, y2: H - pad.b, stroke: "#262b36" }));
|
|
369
346
|
[0, 0.25, 0.5, 0.75, 1].forEach(function (t) {
|
|
370
|
-
var ty = y(t);
|
|
347
|
+
var ty = y(maxDuration * t);
|
|
371
348
|
var lab = svgEl("text", { x: pad.l - 8, y: ty + 3, "text-anchor": "end" });
|
|
372
|
-
lab.textContent = Math.round(
|
|
349
|
+
lab.textContent = Math.round(maxDuration * t) + "s";
|
|
373
350
|
svg.appendChild(lab);
|
|
374
351
|
});
|
|
375
352
|
var lab2 = svgEl("text", { x: W - pad.r, y: H - pad.b + 20, "text-anchor": "end" });
|
|
376
353
|
lab2.textContent = "cost \u2192 (max " + fmtUsd(maxCost) + ")";
|
|
377
354
|
svg.appendChild(lab2);
|
|
378
355
|
data.forEach(function (g) {
|
|
379
|
-
var cx = x(g.cost_usd), cy = y(g.
|
|
356
|
+
var cx = x(g.cost_usd), cy = y(g.avg_elapsed_seconds || 0);
|
|
380
357
|
var r = 6 + Math.min(10, (g.avg_tokens || 0) / 4000);
|
|
381
358
|
var c = svgEl("circle", { cx: cx, cy: cy, r: r, fill: colorFor(g.slug), "fill-opacity": 0.85 });
|
|
382
359
|
svg.appendChild(c);
|
|
@@ -387,6 +364,38 @@
|
|
|
387
364
|
host.appendChild(svg);
|
|
388
365
|
})();
|
|
389
366
|
|
|
367
|
+
// ---------------------------------------------------------------- scatter: cost vs average steps
|
|
368
|
+
(function () {
|
|
369
|
+
var host = document.getElementById("chart-scatter-steps");
|
|
370
|
+
var W = host.clientWidth || 460, H = 260, pad = { l: 42, r: 16, t: 14, b: 30 };
|
|
371
|
+
var svg = svgEl("svg", { width: "100%", height: H, viewBox: "0 0 " + W + " " + H });
|
|
372
|
+
var data = (DATA.leaderboard || []).filter(function (g) { return g.cost_usd != null; });
|
|
373
|
+
var maxCost = Math.max(0.001, Math.max.apply(null, data.map(function (g) { return g.cost_usd; }).concat([0.001])));
|
|
374
|
+
var maxSteps = Math.max(1, Math.max.apply(null, data.map(function (g) { return g.avg_steps || 0; }).concat([1])));
|
|
375
|
+
function x(v) { return pad.l + (v / maxCost) * (W - pad.l - pad.r); }
|
|
376
|
+
function y(v) { return H - pad.b - (v / maxSteps) * (H - pad.t - pad.b); }
|
|
377
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: W - pad.r, y1: H - pad.b, y2: H - pad.b, stroke: "#e4e7eb" }));
|
|
378
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: pad.l, y1: pad.t, y2: H - pad.b, stroke: "#e4e7eb" }));
|
|
379
|
+
[0, 0.25, 0.5, 0.75, 1].forEach(function (t) {
|
|
380
|
+
var ty = y(maxSteps * t);
|
|
381
|
+
var lab = svgEl("text", { x: pad.l - 8, y: ty + 3, "text-anchor": "end" });
|
|
382
|
+
lab.textContent = fmt1(maxSteps * t);
|
|
383
|
+
svg.appendChild(lab);
|
|
384
|
+
});
|
|
385
|
+
var lab2 = svgEl("text", { x: W - pad.r, y: H - pad.b + 20, "text-anchor": "end" });
|
|
386
|
+
lab2.textContent = "cost \u2192 (max " + fmtUsd(maxCost) + ")";
|
|
387
|
+
svg.appendChild(lab2);
|
|
388
|
+
data.forEach(function (g) {
|
|
389
|
+
var cx = x(g.cost_usd), cy = y(g.avg_steps || 0);
|
|
390
|
+
var r = 6 + Math.min(10, (g.avg_tokens || 0) / 4000);
|
|
391
|
+
svg.appendChild(svgEl("circle", { cx: cx, cy: cy, r: r, fill: colorFor(g.slug), "fill-opacity": 0.85 }));
|
|
392
|
+
var label = svgEl("text", { x: cx + r + 4, y: cy + 3 });
|
|
393
|
+
label.textContent = g.slug;
|
|
394
|
+
svg.appendChild(label);
|
|
395
|
+
});
|
|
396
|
+
host.appendChild(svg);
|
|
397
|
+
})();
|
|
398
|
+
|
|
390
399
|
// ---------------------------------------------------------------- per-step line charts
|
|
391
400
|
function lineChart(host, series, valueFn, opts) {
|
|
392
401
|
opts = opts || {};
|
|
@@ -444,6 +453,7 @@
|
|
|
444
453
|
(function () {
|
|
445
454
|
var matrix = DATA.matrix || [];
|
|
446
455
|
var container = document.getElementById("tbl-matrix");
|
|
456
|
+
if (!container) return;
|
|
447
457
|
if (matrix.length === 0) {
|
|
448
458
|
document.getElementById("section-matrix").style.display = "none";
|
|
449
459
|
return;
|
|
@@ -481,6 +491,7 @@
|
|
|
481
491
|
var fModel = document.getElementById("filter-model");
|
|
482
492
|
var fTask = document.getElementById("filter-task");
|
|
483
493
|
var fOutcome = document.getElementById("filter-outcome");
|
|
494
|
+
if (!container || !fModel || !fTask || !fOutcome) return;
|
|
484
495
|
|
|
485
496
|
function uniq(fn) {
|
|
486
497
|
var seen = {}, out = [];
|
package/src/report.js
CHANGED
|
@@ -101,7 +101,9 @@ export function buildSummary(runs) {
|
|
|
101
101
|
solved_rate: rs.length ? solved / rs.length : 0,
|
|
102
102
|
outcomes,
|
|
103
103
|
avg_steps: avg(rs, (r) => r.steps),
|
|
104
|
+
sum_steps: sum(rs, (r) => r.steps),
|
|
104
105
|
avg_elapsed_seconds: avg(rs, (r) => r.elapsed_seconds),
|
|
106
|
+
sum_elapsed_seconds: sum(rs, (r) => r.elapsed_seconds),
|
|
105
107
|
avg_tokens: avg(rs, (r) => r.tokens.total),
|
|
106
108
|
sum_tokens: sum(rs, (r) => r.tokens.total),
|
|
107
109
|
tokens: {
|
|
@@ -221,7 +223,7 @@ export function renderReportHtml(summary) {
|
|
|
221
223
|
if (!tpl.includes("/*__ASTRA_DATA__*/")) {
|
|
222
224
|
throw new Error("src/report.html is missing the /*__ASTRA_DATA__*/ injection marker");
|
|
223
225
|
}
|
|
224
|
-
return tpl.replace("/*__ASTRA_DATA__*/", `window.
|
|
226
|
+
return tpl.replace("/*__ASTRA_DATA__*/", `window.__ASTRA_EMBEDDED_DATA__ = ${data};`);
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
function toRunDoc(run) {
|