benchkit-cli 0.1.0__py3-none-any.whl
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.
- benchkit/__init__.py +1 -0
- benchkit/_pi_proxy.py +219 -0
- benchkit/answer_key_guard.ts +49 -0
- benchkit/artifacts.py +87 -0
- benchkit/benchmarks/__init__.py +105 -0
- benchkit/benchmarks/aider_polyglot.py +405 -0
- benchkit/benchmarks/arc.py +52 -0
- benchkit/benchmarks/banking77.py +149 -0
- benchkit/benchmarks/base.py +19 -0
- benchkit/benchmarks/boolq.py +49 -0
- benchkit/benchmarks/chartqa.py +133 -0
- benchkit/benchmarks/code_verifier.py +77 -0
- benchkit/benchmarks/downloads.py +166 -0
- benchkit/benchmarks/evalplus.py +288 -0
- benchkit/benchmarks/git_surgery.py +485 -0
- benchkit/benchmarks/gpqa.py +56 -0
- benchkit/benchmarks/gsm8k.py +97 -0
- benchkit/benchmarks/hellaswag.py +49 -0
- benchkit/benchmarks/humaneval.py +103 -0
- benchkit/benchmarks/ifeval.py +56 -0
- benchkit/benchmarks/ifeval_instructions.py +477 -0
- benchkit/benchmarks/mbpp.py +60 -0
- benchkit/benchmarks/mc_arena.py +479 -0
- benchkit/benchmarks/mcq.py +94 -0
- benchkit/benchmarks/medfailbench.py +537 -0
- benchkit/benchmarks/mmlu.py +54 -0
- benchkit/benchmarks/mmlu_pro.py +62 -0
- benchkit/benchmarks/mmvp.py +115 -0
- benchkit/benchmarks/ocrbench.py +96 -0
- benchkit/benchmarks/openbookqa.py +52 -0
- benchkit/benchmarks/patcheval.py +929 -0
- benchkit/benchmarks/piqa.py +52 -0
- benchkit/benchmarks/ruler.py +172 -0
- benchkit/benchmarks/ruler_generator.py +535 -0
- benchkit/benchmarks/sanity.py +174 -0
- benchkit/benchmarks/treejs_arena.py +268 -0
- benchkit/benchmarks/truthfulqa.py +52 -0
- benchkit/benchmarks/utils.py +25 -0
- benchkit/benchmarks/vision.py +230 -0
- benchkit/benchmarks/vstar.py +108 -0
- benchkit/benchmarks/winogrande.py +52 -0
- benchkit/benchmarks/xstest.py +116 -0
- benchkit/browser.py +429 -0
- benchkit/catalog.py +121 -0
- benchkit/cli.py +1000 -0
- benchkit/client.py +1413 -0
- benchkit/controls.py +87 -0
- benchkit/datasets/MMLU_LICENSE.txt +21 -0
- benchkit/datasets/README.md +142 -0
- benchkit/datasets/arc_challenge.jsonl +1172 -0
- benchkit/datasets/banking77.jsonl +3080 -0
- benchkit/datasets/banking77_intents.json +79 -0
- benchkit/datasets/boolq.jsonl +3270 -0
- benchkit/datasets/code_verifier.jsonl +500 -0
- benchkit/datasets/gpqa_diamond.jsonl +198 -0
- benchkit/datasets/gsm8k.jsonl +1319 -0
- benchkit/datasets/hellaswag.jsonl +1000 -0
- benchkit/datasets/humaneval.jsonl +164 -0
- benchkit/datasets/ifeval.jsonl +541 -0
- benchkit/datasets/mbpp.jsonl +500 -0
- benchkit/datasets/mc_blocks_1_20_1.jsonl +1003 -0
- benchkit/datasets/medfailbench_hard30.jsonl +30 -0
- benchkit/datasets/medfailbench_hard30.md +13 -0
- benchkit/datasets/openbookqa.jsonl +500 -0
- benchkit/datasets/piqa.jsonl +1838 -0
- benchkit/datasets/sanity_code.jsonl +6 -0
- benchkit/datasets/truthfulqa.jsonl +817 -0
- benchkit/datasets/winogrande.jsonl +1267 -0
- benchkit/datasets/xstest.csv +451 -0
- benchkit/decision.py +194 -0
- benchkit/demo.py +451 -0
- benchkit/dockerfiles.py +247 -0
- benchkit/engine.py +1270 -0
- benchkit/evaluation.py +195 -0
- benchkit/events.py +141 -0
- benchkit/executor.py +96 -0
- benchkit/git_surgery/backport-release-stack/setup.sh +328 -0
- benchkit/git_surgery/backport-release-stack/verify.sh +119 -0
- benchkit/git_surgery/bisect-the-regression/setup.sh +49 -0
- benchkit/git_surgery/bisect-the-regression/verify.sh +34 -0
- benchkit/git_surgery/rebase-conflict-chain/setup.sh +65 -0
- benchkit/git_surgery/rebase-conflict-chain/verify.sh +43 -0
- benchkit/git_surgery/recover-complex-stash/setup.sh +152 -0
- benchkit/git_surgery/recover-complex-stash/verify.sh +95 -0
- benchkit/git_surgery/recover-lost-work/setup.sh +40 -0
- benchkit/git_surgery/recover-lost-work/verify.sh +36 -0
- benchkit/git_surgery/repair-force-pushed-remote/setup.sh +171 -0
- benchkit/git_surgery/repair-force-pushed-remote/verify.sh +83 -0
- benchkit/git_surgery/revert-merge-with-followups/setup.sh +198 -0
- benchkit/git_surgery/revert-merge-with-followups/verify.sh +98 -0
- benchkit/git_surgery/secret-in-history/README.md +27 -0
- benchkit/git_surgery/secret-in-history/setup.sh +107 -0
- benchkit/git_surgery/secret-in-history/verify.sh +106 -0
- benchkit/git_surgery/split-the-mega-commit/setup.sh +50 -0
- benchkit/git_surgery/split-the-mega-commit/verify.sh +48 -0
- benchkit/git_surgery/untangle-nested-submodules/setup.sh +193 -0
- benchkit/git_surgery/untangle-nested-submodules/verify.sh +90 -0
- benchkit/history.py +474 -0
- benchkit/jobs.py +332 -0
- benchkit/leakage.py +51 -0
- benchkit/looping.py +873 -0
- benchkit/mc_render.py +274 -0
- benchkit/mc_viewer/dist/atlas.png +0 -0
- benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
- benchkit/mc_viewer/dist/version.json +4 -0
- benchkit/mc_viewer/dist/viewer.html +23 -0
- benchkit/mc_viewer/dist/viewer.js +2 -0
- benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +25 -0
- benchkit/mc_viewer/dist/worker.js +2 -0
- benchkit/mc_viewer/dist/worker.js.LICENSE.txt +19 -0
- benchkit/metrics.py +100 -0
- benchkit/ollama.py +60 -0
- benchkit/perf.py +353 -0
- benchkit/perf_report.py +207 -0
- benchkit/perturbations.py +208 -0
- benchkit/pi_agent.py +476 -0
- benchkit/pi_package/package-lock.json +1827 -0
- benchkit/pi_package/package.json +8 -0
- benchkit/report.py +142 -0
- benchkit/report_markdown.py +645 -0
- benchkit/results.py +688 -0
- benchkit/retry.py +163 -0
- benchkit/runner.py +769 -0
- benchkit/sandbox.py +1167 -0
- benchkit/template_check.py +291 -0
- benchkit/templates/arena.html +504 -0
- benchkit/templates/history.html +58 -0
- benchkit/templates/perf.html +509 -0
- benchkit/templates/report.html +769 -0
- benchkit/tui/__init__.py +5 -0
- benchkit/tui/app.py +87 -0
- benchkit/tui/app.tcss +512 -0
- benchkit/tui/formatting.py +84 -0
- benchkit/tui/screens/__init__.py +1 -0
- benchkit/tui/screens/connect.py +191 -0
- benchkit/tui/screens/detail.py +288 -0
- benchkit/tui/screens/help.py +84 -0
- benchkit/tui/screens/modals.py +544 -0
- benchkit/tui/screens/results.py +361 -0
- benchkit/tui/screens/run.py +988 -0
- benchkit/tui/screens/setup.py +733 -0
- benchkit/tui/theme.py +135 -0
- benchkit/tui/widgets.py +120 -0
- benchkit_cli-0.1.0.dist-info/METADATA +294 -0
- benchkit_cli-0.1.0.dist-info/RECORD +149 -0
- benchkit_cli-0.1.0.dist-info/WHEEL +5 -0
- benchkit_cli-0.1.0.dist-info/entry_points.txt +3 -0
- benchkit_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- benchkit_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
"""The Markdown report: a summary table, one section per kind of result, and
|
|
2
|
+
every task's prompt, trace and response.
|
|
3
|
+
|
|
4
|
+
Each section writes itself only when some result carries the fields it shows,
|
|
5
|
+
so a plain run gets the table and the task details and nothing else.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
from typing import TextIO
|
|
13
|
+
|
|
14
|
+
from benchkit.metrics import (
|
|
15
|
+
aggregate_tok_s,
|
|
16
|
+
decision_speed,
|
|
17
|
+
effective_concurrency,
|
|
18
|
+
is_decision,
|
|
19
|
+
latency_text,
|
|
20
|
+
stream_tok_s,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _fmt_time(s: float) -> str:
|
|
25
|
+
s = round(s)
|
|
26
|
+
if s >= 60:
|
|
27
|
+
return f"{s // 60}m {s % 60}s"
|
|
28
|
+
return f"{s}s"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def render_markdown(results: list[dict], generated_at: str) -> str:
|
|
32
|
+
"""Render ``results.md`` for one run."""
|
|
33
|
+
f = io.StringIO()
|
|
34
|
+
f.write("# BenchKit Results\n\n")
|
|
35
|
+
f.write(f"**Date:** {generated_at}\n\n")
|
|
36
|
+
for section in _SECTIONS:
|
|
37
|
+
section(f, results)
|
|
38
|
+
f.write("\n---\n\n")
|
|
39
|
+
for result in results:
|
|
40
|
+
_result_details(f, result)
|
|
41
|
+
return f.getvalue()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _contamination(f: TextIO, results: list[dict]) -> None:
|
|
45
|
+
contaminated_rows = [result for result in results if result.get("contaminated")]
|
|
46
|
+
if contaminated_rows:
|
|
47
|
+
f.write("## ⚠️ Contamination detected\n\n")
|
|
48
|
+
language_counts: dict[str, int] = {}
|
|
49
|
+
offending: list[dict] = []
|
|
50
|
+
for result in contaminated_rows:
|
|
51
|
+
for language, count in result.get("contamination_by_language", {}).items():
|
|
52
|
+
language_counts[language] = language_counts.get(language, 0) + int(
|
|
53
|
+
count
|
|
54
|
+
)
|
|
55
|
+
offending.extend(result.get("contaminated_tasks", []))
|
|
56
|
+
f.write(
|
|
57
|
+
"**Headline score excludes "
|
|
58
|
+
f"{sum(language_counts.values())} contaminated task(s).**\n\n"
|
|
59
|
+
)
|
|
60
|
+
f.write(
|
|
61
|
+
"**Per language:** "
|
|
62
|
+
+ " · ".join(
|
|
63
|
+
f"{language} {count}"
|
|
64
|
+
for language, count in sorted(language_counts.items())
|
|
65
|
+
)
|
|
66
|
+
+ "\n\n"
|
|
67
|
+
)
|
|
68
|
+
for task in offending:
|
|
69
|
+
hits = task.get("guard_hits") or []
|
|
70
|
+
targets = sorted(
|
|
71
|
+
{str(match) for hit in hits for match in (hit.get("matches") or [])}
|
|
72
|
+
)
|
|
73
|
+
f.write(
|
|
74
|
+
f"- `{task.get('task_id', '')}`: "
|
|
75
|
+
+ ", ".join(f"`{target}`" for target in targets)
|
|
76
|
+
+ "\n"
|
|
77
|
+
)
|
|
78
|
+
f.write("\n")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _summary_table(f: TextIO, results: list[dict]) -> None:
|
|
82
|
+
f.write(
|
|
83
|
+
"| Model | Harness | Benchmark | Score | Harness score Δ | Loop-kill Δ | Repair Δ | Fixed | Delta | Regressions | Passed | Scored | Total | Fail | Loop killed | Timeout | Length exceeded | Harness error | Tools used/available | Parallel | Trace | Throughput coverage | Agg tok/s | Stream tok/s | Effective | Avg Resp | Wall Time |\n"
|
|
84
|
+
)
|
|
85
|
+
f.write(
|
|
86
|
+
"|-------|---------|-----------|------:|----------------:|------------:|---------:|------:|------:|------------:|-------:|-------:|------:|-----:|------------:|--------:|----------------:|--------------:|---------------------:|---------:|------:|--------------------:|----------:|-------------:|----------:|---------:|----------:|\n"
|
|
87
|
+
)
|
|
88
|
+
for result in results:
|
|
89
|
+
benchmark_label = result.get("benchmark_label", result["benchmark"])
|
|
90
|
+
delta = result.get("score_delta_pp")
|
|
91
|
+
delta_text = f"{float(delta):+.1f}pp" if delta is not None else "—"
|
|
92
|
+
harness_delta = result.get("harness_score_delta_pp")
|
|
93
|
+
harness_delta_text = (
|
|
94
|
+
f"{float(harness_delta):+.1f}pp" if harness_delta is not None else "—"
|
|
95
|
+
)
|
|
96
|
+
repair_delta_text = (
|
|
97
|
+
f"{float(result.get('repair_delta_pp', 0)):+.1f}pp"
|
|
98
|
+
if result.get("repair_attempts")
|
|
99
|
+
else "—"
|
|
100
|
+
)
|
|
101
|
+
repaired_text = (
|
|
102
|
+
f"{result.get('repair_successes', 0)}/{result.get('repair_attempted', 0)}"
|
|
103
|
+
if result.get("repair_attempts")
|
|
104
|
+
else "—"
|
|
105
|
+
)
|
|
106
|
+
regressions = (
|
|
107
|
+
f"{result.get('regressions', 0)}/{result.get('paired_total', 0)}"
|
|
108
|
+
if result.get("perturbation")
|
|
109
|
+
else "—"
|
|
110
|
+
)
|
|
111
|
+
loop_kill_delta = result.get("loop_kill_delta_pp")
|
|
112
|
+
loop_kill_delta_text = (
|
|
113
|
+
f"{float(loop_kill_delta):+.1f}pp" if loop_kill_delta is not None else "—"
|
|
114
|
+
)
|
|
115
|
+
tools_available = len(result.get("pi_tools_available") or [])
|
|
116
|
+
tools_text = (
|
|
117
|
+
f"{result.get('tool_calls', 0)}/{tools_available}"
|
|
118
|
+
if result.get("harness") == "pi"
|
|
119
|
+
else "—"
|
|
120
|
+
)
|
|
121
|
+
f.write(
|
|
122
|
+
f"| {result['model']} | {result.get('harness_label', 'Direct')} "
|
|
123
|
+
f"| {benchmark_label} | {result['score']}% "
|
|
124
|
+
f"| {harness_delta_text} | {loop_kill_delta_text} "
|
|
125
|
+
f"| {repair_delta_text} | {repaired_text} "
|
|
126
|
+
f"| {delta_text} | {regressions} "
|
|
127
|
+
f"| {result['passed']} | {result.get('scored_total', result['total'])} "
|
|
128
|
+
f"| {result['total']} | {result.get('failures', 0)} "
|
|
129
|
+
f"| {result.get('loop_kills', 0)} | {result.get('timeouts', 0)} "
|
|
130
|
+
f"| {result.get('length_exceeded', 0)} "
|
|
131
|
+
f"| {result.get('harness_errors', 0)} | {tools_text} "
|
|
132
|
+
f"| {result.get('concurrency', 1)} "
|
|
133
|
+
f"| {result.get('trace_coverage', 0)}% "
|
|
134
|
+
f"| {result.get('throughput_coverage', 100)}% "
|
|
135
|
+
f"| {decision_speed(result)[0] if is_decision(result) else f'{aggregate_tok_s(result):.1f}'} "
|
|
136
|
+
f"| {'—' if is_decision(result) else f'{stream_tok_s(result):.1f}'} "
|
|
137
|
+
f"| {effective_concurrency(result):.2f}x "
|
|
138
|
+
f"| {latency_text(result)} "
|
|
139
|
+
f"| {_fmt_time(result['total_time'])} |\n"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
f.write(
|
|
143
|
+
"\n**Throughput:** Agg tok/s is covered output tokens divided by job "
|
|
144
|
+
"covered wall time. Stream tok/s uses covered server decode time. "
|
|
145
|
+
"Decision models generate no tokens; their rows show decisions per "
|
|
146
|
+
"second of wall time and median latency instead. "
|
|
147
|
+
"Effective concurrency is summed request time divided by covered wall "
|
|
148
|
+
"time. Throughput coverage is the share of request wall time represented "
|
|
149
|
+
"by completed generations; loop kills, timeouts and harness errors retain "
|
|
150
|
+
"their task token counts but are excluded when complete timing is not "
|
|
151
|
+
"available. Length-exceeded items are included when their token and "
|
|
152
|
+
"timing payload is complete.\n"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _agentic_metrics(f: TextIO, results: list[dict]) -> None:
|
|
157
|
+
agentic_rows = [
|
|
158
|
+
result
|
|
159
|
+
for result in results
|
|
160
|
+
if result.get("tool_schema_validity_rate") is not None
|
|
161
|
+
]
|
|
162
|
+
if agentic_rows:
|
|
163
|
+
f.write("\n## Agentic metrics\n\n")
|
|
164
|
+
f.write(
|
|
165
|
+
"| Model | Benchmark | Schema valid | Avg turns to solve | "
|
|
166
|
+
"Avg tokens to solve | Post-error recovery | Redundant actions | "
|
|
167
|
+
"Destructive actions |\n"
|
|
168
|
+
)
|
|
169
|
+
f.write(
|
|
170
|
+
"|-------|-----------|-------------:|-------------------:|"
|
|
171
|
+
"--------------------:|--------------------:|------------------:|"
|
|
172
|
+
"--------------------:|\n"
|
|
173
|
+
)
|
|
174
|
+
for result in agentic_rows:
|
|
175
|
+
recovery = result.get("post_error_recovery_rate")
|
|
176
|
+
f.write(
|
|
177
|
+
f"| {result['model']} "
|
|
178
|
+
f"| {result.get('benchmark_label', result['benchmark'])} "
|
|
179
|
+
f"| {result['tool_schema_validity_rate']:.1f}% "
|
|
180
|
+
f"| {result.get('avg_turns_to_solve') or '—'} "
|
|
181
|
+
f"| {result.get('avg_tokens_to_solve') or '—'} "
|
|
182
|
+
f"| {f'{recovery:.1f}%' if recovery is not None else '—'} "
|
|
183
|
+
f"| {result.get('redundant_action_rate', 0):.1f}% "
|
|
184
|
+
f"| {result.get('destructive_action_count', 0)} |\n"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _ruler_intervals(f: TextIO, results: list[dict]) -> None:
|
|
189
|
+
ruler_rows = [result for result in results if result.get("task_statistics")]
|
|
190
|
+
if ruler_rows:
|
|
191
|
+
f.write("\n## RULER per-task confidence intervals\n\n")
|
|
192
|
+
f.write("| Model | Context | Task | Samples | Score | 95% CI |\n")
|
|
193
|
+
f.write("|-------|---------|------|--------:|------:|-------:|\n")
|
|
194
|
+
for result in ruler_rows:
|
|
195
|
+
for row in result["task_statistics"]:
|
|
196
|
+
f.write(
|
|
197
|
+
f"| {result['model']} | {result.get('context_label', '')} "
|
|
198
|
+
f"| {row['task']} | {row['samples']} | {row['score']:.1f}% "
|
|
199
|
+
f"| [{row['ci95_low']:.1f}%, {row['ci95_high']:.1f}%] |\n"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _harness_comparison(f: TextIO, results: list[dict]) -> None:
|
|
204
|
+
harness_pairs = [
|
|
205
|
+
result for result in results if result.get("harness_paired_total") is not None
|
|
206
|
+
]
|
|
207
|
+
if harness_pairs:
|
|
208
|
+
f.write("\n## Harness score comparison\n\n")
|
|
209
|
+
f.write(
|
|
210
|
+
"| Model | Benchmark | Feedback | Paired | Direct first | Pi first | Initial score Δ | Direct final | Pi final | Final score Δ | Direct loop kill | Pi loop kill | Loop-kill Δ | Gains | Regressions | Pi version |\n"
|
|
211
|
+
)
|
|
212
|
+
f.write(
|
|
213
|
+
"|-------|-----------|----------|-------:|-------------:|---------:|----------------:|-------------:|---------:|--------------:|-----------------:|-------------:|------------:|------:|------------:|------------|\n"
|
|
214
|
+
)
|
|
215
|
+
for result in harness_pairs:
|
|
216
|
+
f.write(
|
|
217
|
+
f"| {result['model']} "
|
|
218
|
+
f"| {result.get('benchmark_label', result['benchmark'])} "
|
|
219
|
+
f"| {result.get('repair_attempts', 0)} repair turn(s) "
|
|
220
|
+
f"| {result.get('harness_paired_total', 0)} "
|
|
221
|
+
f"| {result.get('direct_first_score', 0):.1f}% "
|
|
222
|
+
f"| {result.get('harness_first_score', 0):.1f}% "
|
|
223
|
+
f"| {result.get('harness_first_score_delta_pp', 0):+.1f}pp "
|
|
224
|
+
f"| {result.get('direct_score', 0):.1f}% "
|
|
225
|
+
f"| {result.get('harness_score', 0):.1f}% "
|
|
226
|
+
f"| {result.get('harness_score_delta_pp', 0):+.1f}pp "
|
|
227
|
+
f"| {result.get('direct_loop_kill_rate', 0):.1f}% "
|
|
228
|
+
f"| {result.get('harness_loop_kill_rate', 0):.1f}% "
|
|
229
|
+
f"| {result.get('loop_kill_delta_pp', 0):+.1f}pp "
|
|
230
|
+
f"| {result.get('harness_gains', 0)} "
|
|
231
|
+
f"| {result.get('harness_regressions', 0)} "
|
|
232
|
+
f"| {result.get('harness_version', 'latest')} |\n"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _repair_effect(f: TextIO, results: list[dict]) -> None:
|
|
237
|
+
repair_rows = [result for result in results if result.get("repair_attempts")]
|
|
238
|
+
if repair_rows:
|
|
239
|
+
f.write("\n## Verifier repair effect\n\n")
|
|
240
|
+
f.write(
|
|
241
|
+
"Multiple-choice rows report the first attempt as their score; "
|
|
242
|
+
"the repaired score appears here only. Questions with two "
|
|
243
|
+
"options are never repaired.\n\n"
|
|
244
|
+
)
|
|
245
|
+
f.write(
|
|
246
|
+
"| Model | Harness | Benchmark | Max repairs | First attempt | Final | Delta | Retried tasks | Repair turns | Repaired |\n"
|
|
247
|
+
)
|
|
248
|
+
f.write(
|
|
249
|
+
"|-------|---------|-----------|------------:|--------------:|------:|------:|--------------:|-------------:|---------:|\n"
|
|
250
|
+
)
|
|
251
|
+
for result in repair_rows:
|
|
252
|
+
f.write(
|
|
253
|
+
f"| {result['model']} "
|
|
254
|
+
f"| {result.get('harness_label', 'Direct')} "
|
|
255
|
+
f"| {result.get('benchmark_label', result['benchmark'])} "
|
|
256
|
+
f"| {result.get('repair_attempts', 0)} "
|
|
257
|
+
f"| {result.get('first_attempt_score', 0):.1f}% "
|
|
258
|
+
f"| {result.get('repaired_score', result.get('score', 0)):.1f}% "
|
|
259
|
+
f"| {result.get('repair_delta_pp', 0):+.1f}pp "
|
|
260
|
+
f"| {result.get('repair_attempted', 0)} "
|
|
261
|
+
f"| {result.get('repair_turns', result.get('repair_attempted', 0))} "
|
|
262
|
+
f"| {result.get('repair_successes', 0)} |\n"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _choice_order_robustness(f: TextIO, results: list[dict]) -> None:
|
|
267
|
+
perturbed = [result for result in results if result.get("perturbation")]
|
|
268
|
+
if perturbed:
|
|
269
|
+
f.write("\n## Choice-order robustness\n\n")
|
|
270
|
+
f.write(
|
|
271
|
+
"| Model | Harness | Benchmark | Seed | Paired | Clean | Perturbed | Delta | Regressions | Recoveries |\n"
|
|
272
|
+
)
|
|
273
|
+
f.write(
|
|
274
|
+
"|-------|---------|-----------|-----:|-------:|------:|----------:|------:|------------:|-----------:|\n"
|
|
275
|
+
)
|
|
276
|
+
for result in perturbed:
|
|
277
|
+
f.write(
|
|
278
|
+
f"| {result['model']} "
|
|
279
|
+
f"| {result.get('harness_label', 'Direct')} "
|
|
280
|
+
f"| {result.get('baseline_benchmark_label', result['benchmark'])} "
|
|
281
|
+
f"| {result.get('perturbation_seed', 42)} "
|
|
282
|
+
f"| {result.get('paired_total', 0)} "
|
|
283
|
+
f"| {result.get('baseline_score', 0):.1f}% "
|
|
284
|
+
f"| {result.get('perturbed_score', 0):.1f}% "
|
|
285
|
+
f"| {result.get('score_delta_pp', 0):+.1f}pp "
|
|
286
|
+
f"| {result.get('regressions', 0)} "
|
|
287
|
+
f"| {result.get('recoveries', 0)} |\n"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _intent_routing(f: TextIO, results: list[dict]) -> None:
|
|
292
|
+
routed = [result for result in results if "macro_f1" in result]
|
|
293
|
+
if routed:
|
|
294
|
+
f.write("\n## Intent routing\n\n")
|
|
295
|
+
f.write(
|
|
296
|
+
"Macro-F1 is the unweighted mean F1 across intents, so a router "
|
|
297
|
+
"that ignores rare intents cannot hide behind the common ones.\n\n"
|
|
298
|
+
)
|
|
299
|
+
f.write(
|
|
300
|
+
"| Model | Harness | Benchmark | Accuracy | Macro-F1 | Speed | Latency |\n"
|
|
301
|
+
)
|
|
302
|
+
f.write(
|
|
303
|
+
"|-------|---------|-----------|---------:|---------:|------:|--------:|\n"
|
|
304
|
+
)
|
|
305
|
+
for result in routed:
|
|
306
|
+
speed, latency = (
|
|
307
|
+
decision_speed(result)
|
|
308
|
+
if is_decision(result)
|
|
309
|
+
else (f"{aggregate_tok_s(result):.1f} tok/s", latency_text(result))
|
|
310
|
+
)
|
|
311
|
+
f.write(
|
|
312
|
+
f"| {result['model']} "
|
|
313
|
+
f"| {result.get('harness_label', 'Direct')} "
|
|
314
|
+
f"| {result.get('benchmark_label', result['benchmark'])} "
|
|
315
|
+
f"| {result.get('score', 0):.1f}% "
|
|
316
|
+
f"| {result['macro_f1']:.1f}% "
|
|
317
|
+
f"| {speed} | {latency} |\n"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _xstest_refusals(f: TextIO, results: list[dict]) -> None:
|
|
322
|
+
xstest_rows = [
|
|
323
|
+
result
|
|
324
|
+
for result in results
|
|
325
|
+
if result.get("xstest_false_refusal_rate") is not None
|
|
326
|
+
or result.get("xstest_missed_refusal_rate") is not None
|
|
327
|
+
]
|
|
328
|
+
if xstest_rows:
|
|
329
|
+
f.write("\n## XSTest refusals\n\n")
|
|
330
|
+
f.write(
|
|
331
|
+
"False refusal: safe prompts refused (over-refusal). Missed "
|
|
332
|
+
"refusal: unsafe prompts answered. Decision rows classify the "
|
|
333
|
+
"prompt; chat rows are graded on their response, so the two are "
|
|
334
|
+
"not comparable.\n\n"
|
|
335
|
+
)
|
|
336
|
+
f.write(
|
|
337
|
+
"| Model | Harness | Score | False refusal | Missed refusal | Safe | Unsafe |\n"
|
|
338
|
+
)
|
|
339
|
+
f.write(
|
|
340
|
+
"|-------|---------|------:|--------------:|---------------:|-----:|-------:|\n"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def rate(value: object) -> str:
|
|
344
|
+
return "—" if value is None else f"{float(value):.1f}%"
|
|
345
|
+
|
|
346
|
+
for result in xstest_rows:
|
|
347
|
+
f.write(
|
|
348
|
+
f"| {result['model']} "
|
|
349
|
+
f"| {result.get('harness_label', 'Direct')} "
|
|
350
|
+
f"| {result.get('score', 0):.1f}% "
|
|
351
|
+
f"| {rate(result.get('xstest_false_refusal_rate'))} "
|
|
352
|
+
f"| {rate(result.get('xstest_missed_refusal_rate'))} "
|
|
353
|
+
f"| {result.get('xstest_safe_total', 0)} "
|
|
354
|
+
f"| {result.get('xstest_unsafe_total', 0)} |\n"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _code_verification(f: TextIO, results: list[dict]) -> None:
|
|
359
|
+
verified = [result for result in results if "verifier_pass_total" in result]
|
|
360
|
+
if verified:
|
|
361
|
+
f.write("\n## Code verification\n\n")
|
|
362
|
+
f.write(
|
|
363
|
+
"Accuracy on solutions that pass their tests and on solutions "
|
|
364
|
+
"that fail them. The suite is balanced, so always answering yes "
|
|
365
|
+
"scores 50% overall; the split shows which way a verifier leans.\n\n"
|
|
366
|
+
)
|
|
367
|
+
f.write(
|
|
368
|
+
"| Model | Harness | Accuracy | On passing | On failing | Speed | Latency |\n"
|
|
369
|
+
)
|
|
370
|
+
f.write(
|
|
371
|
+
"|-------|---------|---------:|-----------:|-----------:|------:|--------:|\n"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def class_rate(value: object) -> str:
|
|
375
|
+
return "—" if value is None else f"{float(value):.1f}%"
|
|
376
|
+
|
|
377
|
+
for result in verified:
|
|
378
|
+
speed, latency = (
|
|
379
|
+
decision_speed(result)
|
|
380
|
+
if is_decision(result)
|
|
381
|
+
else (f"{aggregate_tok_s(result):.1f} tok/s", latency_text(result))
|
|
382
|
+
)
|
|
383
|
+
f.write(
|
|
384
|
+
f"| {result['model']} "
|
|
385
|
+
f"| {result.get('harness_label', 'Direct')} "
|
|
386
|
+
f"| {result.get('score', 0):.1f}% "
|
|
387
|
+
f"| {class_rate(result.get('verifier_pass_accuracy'))} "
|
|
388
|
+
f"| {class_rate(result.get('verifier_fail_accuracy'))} "
|
|
389
|
+
f"| {speed} | {latency} |\n"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _decision_calibration(f: TextIO, results: list[dict]) -> None:
|
|
394
|
+
decided = [result for result in results if "decision_brier" in result]
|
|
395
|
+
if decided:
|
|
396
|
+
f.write("\n## Decision calibration\n\n")
|
|
397
|
+
f.write(
|
|
398
|
+
"Brier: 0 is perfect, 2 is confidently wrong. ECE: gap between "
|
|
399
|
+
"stated confidence and accuracy (10 bins). Dec/s is answered "
|
|
400
|
+
"decisions per second of wall time, including concurrency.\n\n"
|
|
401
|
+
)
|
|
402
|
+
f.write(
|
|
403
|
+
"| Model | Benchmark | Accuracy | Mean confidence | P(correct) | Brier | ECE | Dec/s | p50 | p95 |\n"
|
|
404
|
+
)
|
|
405
|
+
f.write(
|
|
406
|
+
"|-------|-----------|---------:|----------------:|-----------:|------:|----:|------:|----:|----:|\n"
|
|
407
|
+
)
|
|
408
|
+
for result in decided:
|
|
409
|
+
f.write(
|
|
410
|
+
f"| {result['model']} "
|
|
411
|
+
f"| {result.get('benchmark_label', result['benchmark'])} "
|
|
412
|
+
f"| {result.get('score', 0):.1f}% "
|
|
413
|
+
f"| {result['decision_confidence']:.1f}% "
|
|
414
|
+
f"| {result['decision_p_correct']:.1f}% "
|
|
415
|
+
f"| {result['decision_brier']:.4f} "
|
|
416
|
+
f"| {result['decision_ece']:.4f} "
|
|
417
|
+
f"| {result.get('decisions_per_s', 0):.1f} "
|
|
418
|
+
f"| {result.get('decision_latency_p50_ms', 0):.0f} ms "
|
|
419
|
+
f"| {result.get('decision_latency_p95_ms', 0):.0f} ms |\n"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
_SECTIONS = (
|
|
424
|
+
_contamination,
|
|
425
|
+
_summary_table,
|
|
426
|
+
_agentic_metrics,
|
|
427
|
+
_ruler_intervals,
|
|
428
|
+
_harness_comparison,
|
|
429
|
+
_repair_effect,
|
|
430
|
+
_choice_order_robustness,
|
|
431
|
+
_intent_routing,
|
|
432
|
+
_xstest_refusals,
|
|
433
|
+
_code_verification,
|
|
434
|
+
_decision_calibration,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _result_details(f: TextIO, result: dict) -> None:
|
|
439
|
+
benchmark_label = result.get("benchmark_label", result["benchmark"])
|
|
440
|
+
harness = result.get("harness_label", "Direct")
|
|
441
|
+
version = result.get("harness_version")
|
|
442
|
+
version_text = f" {version}" if version else ""
|
|
443
|
+
f.write(f"## {result['model']} / {harness}{version_text} / {benchmark_label}\n\n")
|
|
444
|
+
if result.get("harness") == "pi":
|
|
445
|
+
tools = ", ".join(result.get("pi_tools_available") or []) or "none"
|
|
446
|
+
f.write(
|
|
447
|
+
"**Pi scaffold:** "
|
|
448
|
+
f"system prompt `{result.get('pi_system_prompt_sha256', 'unavailable')}` "
|
|
449
|
+
f"· {result.get('pi_system_prompt_tokens', 'unknown')} tokens "
|
|
450
|
+
f"· {result.get('pi_system_prompt_chars', 0)} chars "
|
|
451
|
+
f"· native tools available: {tools}\n\n"
|
|
452
|
+
)
|
|
453
|
+
if result.get("pi_max_output_tokens"):
|
|
454
|
+
f.write(
|
|
455
|
+
"**Pi output limit:** "
|
|
456
|
+
f"{result['pi_max_output_tokens']} tokens via "
|
|
457
|
+
f"`{result.get('pi_max_output_tokens_field', 'unknown')}`\n\n"
|
|
458
|
+
)
|
|
459
|
+
if result.get("pi_system_prompt"):
|
|
460
|
+
f.write("**Pi system prompt:**\n\n~~~text\n")
|
|
461
|
+
f.write(str(result["pi_system_prompt"]).rstrip())
|
|
462
|
+
f.write("\n~~~\n\n")
|
|
463
|
+
for task in result["tasks"]:
|
|
464
|
+
_task_details(f, task)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _task_details(f: TextIO, task: dict) -> None:
|
|
468
|
+
task_id = task["task_id"]
|
|
469
|
+
entry = task.get("entry_point")
|
|
470
|
+
label = f"{task_id} ({entry})" if entry else task_id
|
|
471
|
+
status = (
|
|
472
|
+
"☣️ CONTAMINATED"
|
|
473
|
+
if task.get("contaminated")
|
|
474
|
+
else "🛑 LOOP KILLED"
|
|
475
|
+
if task.get("loop_killed")
|
|
476
|
+
else "⏱️ TIMEOUT"
|
|
477
|
+
if task.get("timed_out")
|
|
478
|
+
else "📏 LENGTH EXCEEDED"
|
|
479
|
+
if task.get("length_exceeded")
|
|
480
|
+
else "⚠️ HARNESS ERROR"
|
|
481
|
+
if task.get("harness_error")
|
|
482
|
+
else "⚠️ ERROR"
|
|
483
|
+
if task.get("error")
|
|
484
|
+
else "✅ PASS"
|
|
485
|
+
if task["passed"]
|
|
486
|
+
else f"🟨 PARTIAL ({task.get('score', 0):.1f}%)"
|
|
487
|
+
if task.get("score", 0) > 0
|
|
488
|
+
else "❌ FAIL"
|
|
489
|
+
)
|
|
490
|
+
loop = (
|
|
491
|
+
"RECOVERED"
|
|
492
|
+
if task.get("recovered_cycle")
|
|
493
|
+
else task.get("loop_state", "unavailable").upper()
|
|
494
|
+
)
|
|
495
|
+
if (
|
|
496
|
+
loop == "CLEAR"
|
|
497
|
+
and task.get("loop_source") == "answer"
|
|
498
|
+
and task.get("trace_status") == "unavailable"
|
|
499
|
+
):
|
|
500
|
+
loop = "NO TRACE"
|
|
501
|
+
f.write(f"### {label} — {status} · {loop}\n\n")
|
|
502
|
+
f.write(
|
|
503
|
+
f"**Generation:** {task.get('output_tokens', 0)} tokens · "
|
|
504
|
+
f"{task.get('input_tokens', 0)} input · "
|
|
505
|
+
f"{task.get('model_turns', 1)} model turn(s) · "
|
|
506
|
+
f"{task.get('tool_calls', 0)} tool call(s) · "
|
|
507
|
+
f"{task.get('tok_s', 0)} stream tok/s · "
|
|
508
|
+
f"{task.get('response_time_s', 0)}s · "
|
|
509
|
+
f"trace {task.get('trace_status', 'unavailable')} · "
|
|
510
|
+
f"loop score {task.get('loop_score', 0):.1%}\n\n"
|
|
511
|
+
)
|
|
512
|
+
if task.get("error"):
|
|
513
|
+
f.write(f"**Error:** {task['error']}\n\n")
|
|
514
|
+
if workspace := task.get("workspace"):
|
|
515
|
+
_workspace_details(f, task, workspace)
|
|
516
|
+
if attempts := task.get("attempts"):
|
|
517
|
+
_attempt_details(f, attempts)
|
|
518
|
+
if tool_trace := task.get("tool_trace"):
|
|
519
|
+
_tool_trace_details(f, tool_trace)
|
|
520
|
+
if perturbation := task.get("perturbation"):
|
|
521
|
+
order = ", ".join(perturbation.get("choice_order", []))
|
|
522
|
+
f.write(
|
|
523
|
+
"**Perturbation:** choice-order "
|
|
524
|
+
f"(seed {perturbation.get('seed')}) · original labels in "
|
|
525
|
+
f"new order: {order} · answer "
|
|
526
|
+
f"{perturbation.get('original_answer')} → "
|
|
527
|
+
f"{perturbation.get('perturbed_answer')}\n\n"
|
|
528
|
+
)
|
|
529
|
+
f.write("**Prompt:**\n\n")
|
|
530
|
+
f.write(f"~~~\n{task['prompt'].rstrip()}\n~~~\n\n")
|
|
531
|
+
if task.get("thinking"):
|
|
532
|
+
f.write("**Thinking:**\n\n")
|
|
533
|
+
f.write(f"~~~\n{task['thinking'].rstrip()}\n~~~\n\n")
|
|
534
|
+
f.write("**Response:**\n\n")
|
|
535
|
+
f.write(f"~~~\n{task['response'].rstrip()}\n~~~\n\n")
|
|
536
|
+
f.write("---\n\n")
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _workspace_details(f: TextIO, task: dict, workspace: dict) -> None:
|
|
540
|
+
if checkpoints := workspace.get("checkpoints"):
|
|
541
|
+
f.write(
|
|
542
|
+
"**Checkpoints:** "
|
|
543
|
+
f"{workspace.get('positive_points', 0)}/"
|
|
544
|
+
f"{workspace.get('max_points', 0)} points"
|
|
545
|
+
)
|
|
546
|
+
if workspace.get("penalty_points"):
|
|
547
|
+
f.write(f" · −{workspace.get('penalty_points', 0)} penalty")
|
|
548
|
+
f.write("\n\n")
|
|
549
|
+
f.write("| Checkpoint | Weight | Awarded | Evidence |\n")
|
|
550
|
+
f.write("|------------|-------:|--------:|----------|\n")
|
|
551
|
+
for checkpoint in checkpoints:
|
|
552
|
+
evidence = str(checkpoint.get("evidence", "")).replace("|", "\\|")
|
|
553
|
+
f.write(
|
|
554
|
+
f"| {checkpoint.get('id', '')} "
|
|
555
|
+
f"| {checkpoint.get('weight', 0):+} "
|
|
556
|
+
f"| {checkpoint.get('awarded', 0):+} "
|
|
557
|
+
f"| {evidence} |\n"
|
|
558
|
+
)
|
|
559
|
+
f.write("\n")
|
|
560
|
+
command = workspace.get("test_command") or []
|
|
561
|
+
if command:
|
|
562
|
+
f.write(
|
|
563
|
+
"**Workspace verifier:** "
|
|
564
|
+
f"`{' '.join(map(str, command))}` · exit "
|
|
565
|
+
f"{workspace.get('test_exit_code', '—')}\n\n"
|
|
566
|
+
)
|
|
567
|
+
if workspace.get("test_output"):
|
|
568
|
+
f.write("~~~text\n" + str(workspace["test_output"]).rstrip() + "\n~~~\n\n")
|
|
569
|
+
if workspace.get("patch"):
|
|
570
|
+
f.write(
|
|
571
|
+
"**Workspace patch:**\n\n~~~diff\n"
|
|
572
|
+
+ str(workspace["patch"]).rstrip()
|
|
573
|
+
+ "\n~~~\n\n"
|
|
574
|
+
)
|
|
575
|
+
if workspace.get("build_status"):
|
|
576
|
+
f.write(
|
|
577
|
+
f"**Build:** {workspace['build_status']} · "
|
|
578
|
+
f"{workspace.get('blocks_total', 0)} block(s) · "
|
|
579
|
+
f"{workspace.get('palette_size', 0)} block type(s) · "
|
|
580
|
+
f"{workspace.get('hallucinated_blocks', 0)} "
|
|
581
|
+
"non-existent id(s) · "
|
|
582
|
+
f"{workspace.get('blocks_out_of_bounds', 0)} "
|
|
583
|
+
"out of bounds · "
|
|
584
|
+
f"{workspace.get('floating_fraction', 0)} floating\n\n"
|
|
585
|
+
)
|
|
586
|
+
for view in ("iso", "side", "top"):
|
|
587
|
+
if shot := workspace.get(f"screenshot_{view}"):
|
|
588
|
+
f.write(f"\n\n")
|
|
589
|
+
if script := workspace.get("script_py"):
|
|
590
|
+
f.write(f"Build script: [{script}]({script})\n\n")
|
|
591
|
+
elif workspace.get("render_status"):
|
|
592
|
+
f.write(
|
|
593
|
+
f"**Render:** {workspace['render_status']} · "
|
|
594
|
+
f"{len(workspace.get('console_errors') or [])} console "
|
|
595
|
+
f"error(s) · {len(workspace.get('page_errors') or [])} "
|
|
596
|
+
"uncaught · "
|
|
597
|
+
f"{workspace.get('animation_frames', 0)} frame(s)\n\n"
|
|
598
|
+
)
|
|
599
|
+
if shot := workspace.get("screenshot"):
|
|
600
|
+
f.write(f"\n\n")
|
|
601
|
+
if page := workspace.get("page_html"):
|
|
602
|
+
f.write(f"Generated page: [{page}]({page})\n\n")
|
|
603
|
+
diagnostics = [
|
|
604
|
+
*(f"[uncaught] {item}" for item in workspace.get("page_errors") or []),
|
|
605
|
+
*(f"[console] {item}" for item in workspace.get("console_errors") or []),
|
|
606
|
+
*(f"[blocked] {item}" for item in workspace.get("blocked_requests") or []),
|
|
607
|
+
]
|
|
608
|
+
if diagnostics:
|
|
609
|
+
f.write("~~~text\n" + "\n".join(map(str, diagnostics)) + "\n~~~\n\n")
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _attempt_details(f: TextIO, attempts: list[dict]) -> None:
|
|
613
|
+
f.write("**Verifier-feedback attempts:**\n\n")
|
|
614
|
+
for attempt in attempts:
|
|
615
|
+
state = "pass" if attempt.get("passed") else "fail"
|
|
616
|
+
f.write(
|
|
617
|
+
f"#### Attempt {attempt.get('attempt', 1)} — "
|
|
618
|
+
f"{state} · {attempt.get('score', 0):.1f}%\n\n"
|
|
619
|
+
)
|
|
620
|
+
if attempt.get("feedback"):
|
|
621
|
+
f.write(f"Verifier feedback: {attempt['feedback']}\n\n")
|
|
622
|
+
f.write("~~~text\n")
|
|
623
|
+
f.write(str(attempt.get("response") or "").rstrip())
|
|
624
|
+
f.write("\n~~~\n\n")
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _tool_trace_details(f: TextIO, tool_trace: list[dict]) -> None:
|
|
628
|
+
f.write("**Pi native tools:**\n\n")
|
|
629
|
+
for call in tool_trace:
|
|
630
|
+
state = "error" if call.get("is_error") else "ok"
|
|
631
|
+
duration = float(call.get("duration_s") or 0)
|
|
632
|
+
f.write(f"- `{call.get('name', 'tool')}` · {state} · {duration:.3f}s\n\n")
|
|
633
|
+
f.write(
|
|
634
|
+
"~~~json\n"
|
|
635
|
+
+ json.dumps(
|
|
636
|
+
call.get("arguments") or {},
|
|
637
|
+
ensure_ascii=False,
|
|
638
|
+
indent=2,
|
|
639
|
+
).rstrip()
|
|
640
|
+
+ "\n~~~\n\n"
|
|
641
|
+
)
|
|
642
|
+
if call.get("output"):
|
|
643
|
+
suffix = " (last 8,000 chars)" if call.get("output_truncated") else ""
|
|
644
|
+
f.write(f"Tool output{suffix}:\n\n")
|
|
645
|
+
f.write(f"~~~\n{str(call['output']).rstrip()}\n~~~\n\n")
|