Spaces:
Sleeping
Sleeping
Redeploy ELBench-live
Browse files- dashboard/app.py +343 -60
- docs/DASHBOARD_TARGETS.md +143 -0
- src/elbench/dashboard/english.py +5 -5
- src/elbench/dashboard/leaderboard.py +222 -67
- src/elbench/dashboard/plan.py +70 -4
- src/elbench/dashboard/protocol.py +10 -8
- src/elbench/dashboard/routing.py +148 -0
- src/elbench/dashboard/run.py +6 -0
- src/elbench/execution/basic_education.py +20 -9
- src/elbench/execution/runner.py +13 -1
- src/elbench/judges/llm_judge.py +3 -5
- tests/test_dashboard_faithfulness.py +283 -0
dashboard/app.py
CHANGED
|
@@ -28,6 +28,14 @@ import httpx
|
|
| 28 |
from elbench.dashboard.catalog import Catalog, CatalogFile, load_catalog
|
| 29 |
from elbench.dashboard.english import AREA_LABELS, area_label, english_dimension, plain_file
|
| 30 |
from elbench.dashboard.leaderboard import (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
LeaderboardStore,
|
| 32 |
build_comparison_choices,
|
| 33 |
build_comparison_table,
|
|
@@ -38,11 +46,13 @@ from elbench.dashboard.leaderboard import (
|
|
| 38 |
official_per_file_rows,
|
| 39 |
pareto_cost_metric_choices,
|
| 40 |
pareto_score_metric_choices,
|
|
|
|
| 41 |
seed_official_openrouter_prices,
|
| 42 |
)
|
| 43 |
from elbench.dashboard.model_sizes import ModelSize, load_sizes, model_size
|
| 44 |
-
from elbench.dashboard.plan import Plan, build_plan, render_plan
|
| 45 |
from elbench.dashboard.protocol import DEFAULT_SYSTEM_ONE_MODELS, catalog_slug
|
|
|
|
| 46 |
from elbench.dashboard.run import (
|
| 47 |
execute_run,
|
| 48 |
failures_progress_path,
|
|
@@ -58,7 +68,7 @@ OPENROUTER_DECISION_MODELS_URL = "https://openrouter.ai/api/v1/models?output_mod
|
|
| 58 |
OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key"
|
| 59 |
OPENROUTER_KEYS_PAGE = "https://openrouter.ai/settings/keys"
|
| 60 |
DEFAULT_TARGET_SLUG = "openai/gpt-5.4"
|
| 61 |
-
DEFAULT_CHAT_JUDGE_SLUG =
|
| 62 |
DEFAULT_SYSTEM_ONE_JUDGE_SLUG = "liquid/d1-ultra"
|
| 63 |
DEFAULT_MULTITURN_CHAT_SLUG = "liquid/lfm-2.5-24b-a2b"
|
| 64 |
DEFAULT_BUDGET_USD = 5.0
|
|
@@ -72,7 +82,7 @@ ELBENCH_RESULTS_URL = "https://huggingface.co/datasets/ZeroLoss-Lab/ELBench-resu
|
|
| 72 |
PRESET_QUICK = "quick"
|
| 73 |
PRESET_AUTO = "auto"
|
| 74 |
PRESET_FULL = "full"
|
| 75 |
-
|
| 76 |
|
| 77 |
# Filter sliders: the right-hand end means "no limit".
|
| 78 |
PRICE_FILTER_MAX = 30.0
|
|
@@ -286,6 +296,14 @@ def _price_pair(prices: dict, model_id: str) -> tuple[float | None, float | None
|
|
| 286 |
return raw[0], raw[1]
|
| 287 |
|
| 288 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
def _pick_preferred(choices: list[tuple[str, str]], preferred: str) -> str:
|
| 290 |
slugs = [slug for _, slug in choices]
|
| 291 |
if preferred in slugs or not slugs:
|
|
@@ -346,37 +364,58 @@ def _selected_entries(names: list[str] | None) -> list[CatalogFile]:
|
|
| 346 |
return [entry for entry in CATALOG.files if entry.canonical_name in wanted]
|
| 347 |
|
| 348 |
|
| 349 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
if CATALOG is None:
|
| 351 |
return []
|
| 352 |
-
|
| 353 |
-
|
|
|
|
| 354 |
return [
|
| 355 |
-
(f"Quick check · {
|
| 356 |
(f"Automatically checked set · {auto_items:,} questions, no judge model needed", PRESET_AUTO),
|
| 357 |
-
(f"
|
| 358 |
]
|
| 359 |
|
| 360 |
|
| 361 |
-
def apply_preset(preset: str):
|
| 362 |
if CATALOG is None:
|
| 363 |
-
return gr.update(), gr.update(), gr.update(), gr.update()
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
files = CATALOG.default_files()
|
| 370 |
-
modules = CATALOG.default_modules()
|
| 371 |
-
cap = QUICK_CHECK_ITEMS if preset == PRESET_QUICK else None
|
| 372 |
return (
|
| 373 |
gr.update(value=modules),
|
| 374 |
gr.update(value=[entry.canonical_name for entry in files]),
|
| 375 |
gr.update(choices=_dimension_choices(files), value=[]),
|
| 376 |
-
gr.update(value=
|
|
|
|
| 377 |
)
|
| 378 |
|
| 379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
def on_modules(modules: list[str]):
|
| 381 |
if CATALOG is None:
|
| 382 |
return gr.update(), gr.update()
|
|
@@ -493,12 +532,23 @@ def _make_plan(
|
|
| 493 |
files,
|
| 494 |
dimensions,
|
| 495 |
max_samples,
|
|
|
|
| 496 |
concurrency,
|
| 497 |
prices,
|
| 498 |
) -> Plan:
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
return build_plan(
|
| 503 |
CATALOG,
|
| 504 |
openrouter_model=str(model_id).strip(),
|
|
@@ -506,6 +556,7 @@ def _make_plan(
|
|
| 506 |
source_files=set(files or []),
|
| 507 |
dimensions=set(dimensions or []),
|
| 508 |
max_samples=_max_samples(max_samples),
|
|
|
|
| 509 |
prompt_usd_per_token=prompt_price,
|
| 510 |
completion_usd_per_token=completion_price,
|
| 511 |
max_concurrency=int(concurrency),
|
|
@@ -517,6 +568,16 @@ def _make_plan(
|
|
| 517 |
multiturn_openrouter_model=str(multiturn_model_id or "").strip() if use_multiturn_chat else None,
|
| 518 |
multiturn_prompt_usd_per_token=mt_prompt_price,
|
| 519 |
multiturn_completion_usd_per_token=mt_completion_price,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
)
|
| 521 |
|
| 522 |
|
|
@@ -555,13 +616,23 @@ def plain_estimate(
|
|
| 555 |
questions = plan.standard_requests + plan.basic_tasks
|
| 556 |
lines.append(f"### {questions:,} questions for `{plan.protocol.openrouter_model}`")
|
| 557 |
graded = plan.judge_calls + plan.basic_tasks
|
| 558 |
-
if graded and plan.judge_model_config is not None:
|
|
|
|
|
|
|
| 559 |
lines.append(
|
| 560 |
f"{graded:,} answers are graded by `{plan.judge_model_config.model_name}`; "
|
| 561 |
"the rest are checked automatically against the answer key."
|
| 562 |
)
|
| 563 |
else:
|
| 564 |
lines.append("Every answer is checked automatically against the answer key.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
lines.append("")
|
| 566 |
|
| 567 |
if plan.low_usd is None or plan.high_usd is None:
|
|
@@ -574,6 +645,12 @@ def plain_estimate(
|
|
| 574 |
f"**Estimated cost: {_usd(plan.low_usd)} to {_usd(plan.high_usd)}.** The low end counts the "
|
| 575 |
"questions only; the high end assumes every answer runs to the length limit, so real runs "
|
| 576 |
"land well below it."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
)
|
| 578 |
for role in ("target", "judge"):
|
| 579 |
if role in plan.component_usd:
|
|
@@ -590,6 +667,15 @@ def plain_estimate(
|
|
| 590 |
)
|
| 591 |
lines.append("")
|
| 592 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
problems = budget_problems(plan, total_budget, role_budgets)
|
| 594 |
if problems:
|
| 595 |
lines.append("**Spending limit too low:**")
|
|
@@ -609,8 +695,13 @@ def plain_estimate(
|
|
| 609 |
)
|
| 610 |
lines.append("")
|
| 611 |
|
| 612 |
-
if plan.
|
| 613 |
-
lines.append("This is a
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
else:
|
| 615 |
lines.append(
|
| 616 |
"This is a partial run. Its results are saved with the visitor runs, but the chart only shows "
|
|
@@ -641,6 +732,7 @@ def estimate(
|
|
| 641 |
files,
|
| 642 |
dimensions,
|
| 643 |
max_samples,
|
|
|
|
| 644 |
concurrency,
|
| 645 |
prices,
|
| 646 |
total_budget,
|
|
@@ -672,6 +764,7 @@ def estimate(
|
|
| 672 |
files=files,
|
| 673 |
dimensions=dimensions,
|
| 674 |
max_samples=max_samples,
|
|
|
|
| 675 |
concurrency=concurrency,
|
| 676 |
prices=prices,
|
| 677 |
)
|
|
@@ -724,6 +817,7 @@ def run_evaluation(
|
|
| 724 |
files,
|
| 725 |
dimensions,
|
| 726 |
max_samples,
|
|
|
|
| 727 |
concurrency,
|
| 728 |
prices,
|
| 729 |
total_budget,
|
|
@@ -777,6 +871,7 @@ def run_evaluation(
|
|
| 777 |
files=files,
|
| 778 |
dimensions=dimensions,
|
| 779 |
max_samples=sample_cap,
|
|
|
|
| 780 |
concurrency=worker_concurrency,
|
| 781 |
prices=prices,
|
| 782 |
)
|
|
@@ -873,10 +968,19 @@ def run_evaluation(
|
|
| 873 |
lines = [heading, "", body]
|
| 874 |
if scores:
|
| 875 |
lines += ["", f"Scores on the questions that ran: {scores}"]
|
|
|
|
|
|
|
| 876 |
if persist_to_leaderboard:
|
| 877 |
lines += ["", "Saved to the visitor-run leaderboard."]
|
| 878 |
-
|
| 879 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 880 |
else:
|
| 881 |
lines += ["", "Not saved to the leaderboard (you opted out)."]
|
| 882 |
lines += ["", f"<sub>Run id `{result.get('run_id')}`</sub>"]
|
|
@@ -900,6 +1004,8 @@ def leaderboard_view(
|
|
| 900 |
max_price,
|
| 901 |
max_size,
|
| 902 |
include_undisclosed,
|
|
|
|
|
|
|
| 903 |
prices,
|
| 904 |
):
|
| 905 |
runs = LEADERBOARD_STORE.load_runs()
|
|
@@ -920,8 +1026,32 @@ def leaderboard_view(
|
|
| 920 |
cost_metric=cost_metric or "blended_per_1m",
|
| 921 |
include_custom_runs=bool(include_visitor_runs),
|
| 922 |
keep=keep,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 923 |
)
|
| 924 |
-
return fig, rows, f"{md}\n\n<sub>{label}</sub>"
|
| 925 |
|
| 926 |
|
| 927 |
def refresh_leaderboards(
|
|
@@ -932,28 +1062,43 @@ def refresh_leaderboards(
|
|
| 932 |
max_price,
|
| 933 |
max_size,
|
| 934 |
include_undisclosed,
|
|
|
|
|
|
|
| 935 |
primary_sel,
|
| 936 |
baseline_sels,
|
| 937 |
granularity,
|
| 938 |
prices,
|
| 939 |
):
|
| 940 |
runs = LEADERBOARD_STORE.sync_from_hub()
|
|
|
|
| 941 |
fig, rows, md = leaderboard_view(
|
| 942 |
-
focus,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 943 |
)
|
| 944 |
-
primary_choices, baseline_choices = build_comparison_choices(runs)
|
| 945 |
keys = [value for _, value in primary_choices]
|
| 946 |
resolved_primary = primary_sel if primary_sel in keys else (keys[0] if keys else None)
|
| 947 |
resolved_baselines = [b for b in (baseline_sels or []) if b in keys] or [
|
| 948 |
key for key in ("official:deepseek-v4-flash", "official:gpt-5.4") if key in keys and key != resolved_primary
|
| 949 |
]
|
| 950 |
-
headers, comp_rows, comp_note = build_comparison_table(
|
|
|
|
|
|
|
| 951 |
return (
|
| 952 |
fig,
|
| 953 |
rows,
|
| 954 |
md,
|
| 955 |
-
custom_leaderboard_rows(runs),
|
| 956 |
f"{len(runs)} visitor run(s). {LEADERBOARD_STORE.sync_status}",
|
|
|
|
| 957 |
gr.update(choices=primary_choices, value=resolved_primary),
|
| 958 |
gr.update(choices=baseline_choices, value=resolved_baselines),
|
| 959 |
gr.Dataframe(headers=headers, value=comp_rows, wrap=True),
|
|
@@ -961,9 +1106,63 @@ def refresh_leaderboards(
|
|
| 961 |
)
|
| 962 |
|
| 963 |
|
| 964 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 965 |
headers, comp_rows, comp_note = build_comparison_table(
|
| 966 |
-
primary_sel, baseline_sels, granularity, LEADERBOARD_STORE.load_runs()
|
| 967 |
)
|
| 968 |
return gr.Dataframe(headers=headers, value=comp_rows, wrap=True), comp_note
|
| 969 |
|
|
@@ -971,7 +1170,7 @@ def update_comparison(primary_sel, baseline_sels, granularity):
|
|
| 971 |
def _about_markdown() -> str:
|
| 972 |
counts: dict[str, int] = {}
|
| 973 |
if CATALOG is not None:
|
| 974 |
-
for entry in CATALOG.files:
|
| 975 |
counts[entry.module] = counts.get(entry.module, 0) + entry.item_count
|
| 976 |
rows = "\n".join(
|
| 977 |
f"| **{label}** | {counts.get(module, 0):,} | {description} |"
|
|
@@ -987,22 +1186,48 @@ area by area too.
|
|
| 987 |
|:-----|:---------:|:---------------|
|
| 988 |
{rows}
|
| 989 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 990 |
## How the scores work
|
| 991 |
|
| 992 |
- Each area is scored from 0 to 100. **Overall** is the average of the four areas.
|
| 993 |
- Questions with a single right answer are checked automatically. Open-ended answers (safety guidance,
|
| 994 |
teaching, character and thinking skills) are graded by a judge model against a rubric.
|
| 995 |
-
- The published results include a 95% range from 10,000 bootstrap resamples
|
| 996 |
-
overlap, the difference between them could disappear on a re-run, and the
|
|
|
|
| 997 |
- **Best value** models score higher than every model that costs less. Price is the OpenRouter price per
|
| 998 |
million tokens, weighting input three times as heavily as output, a common way to compare models.
|
| 999 |
|
| 1000 |
## Visitor runs
|
| 1001 |
|
| 1002 |
- Runs use the same questions, prompts, retries and scoring code as the paper.
|
| 1003 |
-
- Models are called through OpenRouter
|
|
|
|
|
|
|
| 1004 |
- The default judge is Qwen3-235B on OpenRouter. The paper used its own hosted Qwen judge, so judged scores
|
| 1005 |
-
from visitor runs can differ
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1006 |
- Only runs that answered every question in an area appear on the chart for that area, so partial runs never
|
| 1007 |
compete with complete ones.
|
| 1008 |
|
|
@@ -1069,16 +1294,17 @@ def build_demo() -> gr.Blocks:
|
|
| 1069 |
initial_runs = LEADERBOARD_STORE.load_runs()
|
| 1070 |
initial_prices, _ = price_snapshot()
|
| 1071 |
init_fig, init_rows, init_md = leaderboard_view(
|
| 1072 |
-
"overall", "", "blended_per_1m", True, PRICE_FILTER_MAX, SIZE_FILTER_MAX, True,
|
|
|
|
| 1073 |
)
|
| 1074 |
-
primary_choices, baseline_choices = build_comparison_choices(initial_runs)
|
| 1075 |
default_primary = primary_choices[0][1] if primary_choices else "official:gpt-5.4"
|
| 1076 |
default_baselines = [k for k in ("official:deepseek-v4-flash", "official:gpt-5.4") if k != default_primary]
|
| 1077 |
granularity_default = "By Source File / Task (Matched Subset)"
|
| 1078 |
init_comp_headers, init_comp_rows, init_comp_note = build_comparison_table(
|
| 1079 |
-
default_primary, default_baselines, granularity_default, initial_runs
|
| 1080 |
)
|
| 1081 |
-
default_files =
|
| 1082 |
|
| 1083 |
with gr.Blocks(title="ELBench-live") as demo:
|
| 1084 |
prices = gr.State(initial_prices)
|
|
@@ -1087,11 +1313,23 @@ def build_demo() -> gr.Blocks:
|
|
| 1087 |
with gr.Tab("Leaderboard", id="leaderboard"):
|
| 1088 |
gr.Markdown(
|
| 1089 |
"# How well do AI models handle education?\n"
|
| 1090 |
-
"ELBench scores AI models on
|
| 1091 |
-
"**knowledge & reasoning**, and **character & thinking skills**.
|
| 1092 |
-
"higher scores better, further left costs less."
|
| 1093 |
)
|
| 1094 |
focus = gr.Radio(label="Area", choices=FOCUS_CHOICES, value="overall")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1095 |
chart_max_price, chart_max_size, chart_undisclosed = _filter_controls()
|
| 1096 |
chart = gr.Plot(value=init_fig, show_label=False)
|
| 1097 |
takeaway = gr.Markdown(value=init_md)
|
|
@@ -1121,8 +1359,8 @@ def build_demo() -> gr.Blocks:
|
|
| 1121 |
show_label=False,
|
| 1122 |
)
|
| 1123 |
|
| 1124 |
-
with gr.Accordion("Published results
|
| 1125 |
-
gr.Dataframe(
|
| 1126 |
headers=[
|
| 1127 |
"Rank",
|
| 1128 |
"Model",
|
|
@@ -1133,7 +1371,7 @@ def build_demo() -> gr.Blocks:
|
|
| 1133 |
"Character & thinking",
|
| 1134 |
"Questions",
|
| 1135 |
],
|
| 1136 |
-
value=official_leaderboard_rows(),
|
| 1137 |
wrap=True,
|
| 1138 |
show_label=False,
|
| 1139 |
)
|
|
@@ -1145,7 +1383,7 @@ def build_demo() -> gr.Blocks:
|
|
| 1145 |
)
|
| 1146 |
official_file_table = gr.Dataframe(
|
| 1147 |
headers=["Model", "Area", "Task", "Questions", "Score", "Pass rate (%)", "Mean raw score"],
|
| 1148 |
-
value=official_per_file_rows("GPT-5.4"),
|
| 1149 |
wrap=True,
|
| 1150 |
show_label=False,
|
| 1151 |
)
|
|
@@ -1168,7 +1406,7 @@ def build_demo() -> gr.Blocks:
|
|
| 1168 |
"Teaching",
|
| 1169 |
"Character & thinking",
|
| 1170 |
],
|
| 1171 |
-
value=custom_leaderboard_rows(initial_runs),
|
| 1172 |
wrap=True,
|
| 1173 |
show_label=False,
|
| 1174 |
)
|
|
@@ -1222,7 +1460,12 @@ def build_demo() -> gr.Blocks:
|
|
| 1222 |
with gr.Accordion("Narrow the list by price or size", open=False):
|
| 1223 |
pick_max_price, pick_max_size, pick_undisclosed = _filter_controls()
|
| 1224 |
models_shown = gr.Markdown()
|
| 1225 |
-
preset = gr.Radio(label="How much to run", choices=_preset_choices(), value=PRESET_QUICK)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1226 |
with gr.Row():
|
| 1227 |
total_budget = gr.Number(
|
| 1228 |
label="Spending limit (USD)",
|
|
@@ -1245,7 +1488,8 @@ def build_demo() -> gr.Blocks:
|
|
| 1245 |
value=DEFAULT_CHAT_JUDGE_SLUG,
|
| 1246 |
allow_custom_value=True,
|
| 1247 |
filterable=True,
|
| 1248 |
-
info="Grades open-ended answers. The default is the closest OpenRouter match to the paper's
|
|
|
|
| 1249 |
)
|
| 1250 |
use_system_one_judge = gr.Checkbox(
|
| 1251 |
label="Use a decision model as the judge",
|
|
@@ -1276,7 +1520,7 @@ def build_demo() -> gr.Blocks:
|
|
| 1276 |
modules = gr.CheckboxGroup(
|
| 1277 |
label="Areas",
|
| 1278 |
choices=_module_choices(),
|
| 1279 |
-
value=
|
| 1280 |
info="Choosing an area selects all of its tasks.",
|
| 1281 |
)
|
| 1282 |
files = gr.CheckboxGroup(
|
|
@@ -1290,9 +1534,15 @@ def build_demo() -> gr.Blocks:
|
|
| 1290 |
value=[],
|
| 1291 |
)
|
| 1292 |
with gr.Row():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1293 |
max_samples = gr.Number(
|
| 1294 |
label="Stop after this many questions",
|
| 1295 |
-
value=
|
| 1296 |
precision=0,
|
| 1297 |
info="Leave empty to run every selected question.",
|
| 1298 |
)
|
|
@@ -1337,19 +1587,46 @@ def build_demo() -> gr.Blocks:
|
|
| 1337 |
chart_max_price,
|
| 1338 |
chart_max_size,
|
| 1339 |
chart_undisclosed,
|
|
|
|
|
|
|
| 1340 |
]
|
| 1341 |
view_outputs = [chart, value_table, takeaway]
|
| 1342 |
-
for control in view_controls:
|
| 1343 |
control.change(leaderboard_view, view_controls + [prices], view_outputs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1344 |
refresh.click(
|
| 1345 |
refresh_leaderboards,
|
| 1346 |
view_controls + [comp_primary, comp_baselines, comp_granularity, prices],
|
| 1347 |
-
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1348 |
)
|
| 1349 |
go_test.click(lambda: gr.Tabs(selected="test"), None, tabs)
|
| 1350 |
-
official_model_filter.change(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1351 |
for control in (comp_primary, comp_baselines, comp_granularity):
|
| 1352 |
-
control.change(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1353 |
|
| 1354 |
# Test-a-model events
|
| 1355 |
pick_filters = [pick_max_price, pick_max_size, pick_undisclosed]
|
|
@@ -1362,7 +1639,12 @@ def build_demo() -> gr.Blocks:
|
|
| 1362 |
control.change(filter_target_models, [model] + pick_filters, [model, models_shown])
|
| 1363 |
use_system_one_judge.change(on_toggle_system_one, [use_system_one_judge], [judge_model])
|
| 1364 |
use_multiturn_chat.change(on_toggle_multiturn, [use_multiturn_chat], [multiturn_model])
|
| 1365 |
-
preset.change(apply_preset, [preset], [modules, files, dimensions, max_samples])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1366 |
modules.input(on_modules, [modules], [files, dimensions])
|
| 1367 |
files.input(on_files, [files], [dimensions])
|
| 1368 |
run_inputs = [
|
|
@@ -1376,6 +1658,7 @@ def build_demo() -> gr.Blocks:
|
|
| 1376 |
files,
|
| 1377 |
dimensions,
|
| 1378 |
max_samples,
|
|
|
|
| 1379 |
concurrency,
|
| 1380 |
prices,
|
| 1381 |
total_budget,
|
|
|
|
| 28 |
from elbench.dashboard.catalog import Catalog, CatalogFile, load_catalog
|
| 29 |
from elbench.dashboard.english import AREA_LABELS, area_label, english_dimension, plain_file
|
| 30 |
from elbench.dashboard.leaderboard import (
|
| 31 |
+
PAPER_JUDGE_LABEL,
|
| 32 |
+
PAPER_JUDGE_SLUG,
|
| 33 |
+
judge_board,
|
| 34 |
+
judge_board_choices,
|
| 35 |
+
judge_board_label,
|
| 36 |
+
CHINA_SPECIFIC_FILES,
|
| 37 |
+
SCOPE_CORE,
|
| 38 |
+
SCOPE_PAPER,
|
| 39 |
LeaderboardStore,
|
| 40 |
build_comparison_choices,
|
| 41 |
build_comparison_table,
|
|
|
|
| 46 |
official_per_file_rows,
|
| 47 |
pareto_cost_metric_choices,
|
| 48 |
pareto_score_metric_choices,
|
| 49 |
+
scope_item_count,
|
| 50 |
seed_official_openrouter_prices,
|
| 51 |
)
|
| 52 |
from elbench.dashboard.model_sizes import ModelSize, load_sizes, model_size
|
| 53 |
+
from elbench.dashboard.plan import JUDGE_HIGH_OUTPUT_TOKENS, Plan, build_plan, render_plan
|
| 54 |
from elbench.dashboard.protocol import DEFAULT_SYSTEM_ONE_MODELS, catalog_slug
|
| 55 |
+
from elbench.dashboard.routing import Route, resolve_route
|
| 56 |
from elbench.dashboard.run import (
|
| 57 |
execute_run,
|
| 58 |
failures_progress_path,
|
|
|
|
| 68 |
OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key"
|
| 69 |
OPENROUTER_KEYS_PAGE = "https://openrouter.ai/settings/keys"
|
| 70 |
DEFAULT_TARGET_SLUG = "openai/gpt-5.4"
|
| 71 |
+
DEFAULT_CHAT_JUDGE_SLUG = PAPER_JUDGE_SLUG
|
| 72 |
DEFAULT_SYSTEM_ONE_JUDGE_SLUG = "liquid/d1-ultra"
|
| 73 |
DEFAULT_MULTITURN_CHAT_SLUG = "liquid/lfm-2.5-24b-a2b"
|
| 74 |
DEFAULT_BUDGET_USD = 5.0
|
|
|
|
| 82 |
PRESET_QUICK = "quick"
|
| 83 |
PRESET_AUTO = "auto"
|
| 84 |
PRESET_FULL = "full"
|
| 85 |
+
QUICK_CHECK_PER_TASK = 1
|
| 86 |
|
| 87 |
# Filter sliders: the right-hand end means "no limit".
|
| 88 |
PRICE_FILTER_MAX = 30.0
|
|
|
|
| 296 |
return raw[0], raw[1]
|
| 297 |
|
| 298 |
|
| 299 |
+
def _routed_price(route: Route | None, fallback: tuple[float | None, float | None]) -> tuple[float | None, float | None]:
|
| 300 |
+
"""Price of the host the request goes to first, else the catalog price."""
|
| 301 |
+
host = route.cheapest if route is not None else None
|
| 302 |
+
if host is None:
|
| 303 |
+
return fallback
|
| 304 |
+
return host.prompt_usd_per_token, host.completion_usd_per_token
|
| 305 |
+
|
| 306 |
+
|
| 307 |
def _pick_preferred(choices: list[tuple[str, str]], preferred: str) -> str:
|
| 308 |
slugs = [slug for _, slug in choices]
|
| 309 |
if preferred in slugs or not slugs:
|
|
|
|
| 364 |
return [entry for entry in CATALOG.files if entry.canonical_name in wanted]
|
| 365 |
|
| 366 |
|
| 367 |
+
def _scope(include_china: bool) -> str:
|
| 368 |
+
return SCOPE_PAPER if include_china else SCOPE_CORE
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def _in_scope(files: list[CatalogFile], include_china: bool) -> list[CatalogFile]:
|
| 372 |
+
if include_china:
|
| 373 |
+
return files
|
| 374 |
+
return [entry for entry in files if entry.canonical_name not in CHINA_SPECIFIC_FILES]
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def _preset_files(preset: str, include_china: bool) -> list[CatalogFile]:
|
| 378 |
+
if CATALOG is None:
|
| 379 |
+
return []
|
| 380 |
+
if preset == PRESET_AUTO:
|
| 381 |
+
return _in_scope(CATALOG.default_files(), include_china)
|
| 382 |
+
return _in_scope(list(CATALOG.files), include_china)
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def _preset_choices(include_china: bool = False) -> list[tuple[str, str]]:
|
| 386 |
if CATALOG is None:
|
| 387 |
return []
|
| 388 |
+
quick_items = len(_preset_files(PRESET_QUICK, include_china)) * QUICK_CHECK_PER_TASK
|
| 389 |
+
auto_items = sum(entry.item_count for entry in _preset_files(PRESET_AUTO, include_china))
|
| 390 |
+
full_items = sum(entry.item_count for entry in _preset_files(PRESET_FULL, include_china))
|
| 391 |
return [
|
| 392 |
+
(f"Quick check · {quick_items} questions, one from each task, to see that everything works", PRESET_QUICK),
|
| 393 |
(f"Automatically checked set · {auto_items:,} questions, no judge model needed", PRESET_AUTO),
|
| 394 |
+
(f"Complete benchmark · all {full_items:,} questions · appears on the leaderboard chart", PRESET_FULL),
|
| 395 |
]
|
| 396 |
|
| 397 |
|
| 398 |
+
def apply_preset(preset: str, include_china: bool = False):
|
| 399 |
if CATALOG is None:
|
| 400 |
+
return gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
|
| 401 |
+
files = _preset_files(preset, include_china)
|
| 402 |
+
modules: list[str] = []
|
| 403 |
+
for entry in files:
|
| 404 |
+
if entry.module not in modules:
|
| 405 |
+
modules.append(entry.module)
|
|
|
|
|
|
|
|
|
|
| 406 |
return (
|
| 407 |
gr.update(value=modules),
|
| 408 |
gr.update(value=[entry.canonical_name for entry in files]),
|
| 409 |
gr.update(choices=_dimension_choices(files), value=[]),
|
| 410 |
+
gr.update(value=None),
|
| 411 |
+
gr.update(value=QUICK_CHECK_PER_TASK if preset == PRESET_QUICK else None),
|
| 412 |
)
|
| 413 |
|
| 414 |
|
| 415 |
+
def on_include_china(preset: str, include_china: bool):
|
| 416 |
+
return (gr.update(choices=_preset_choices(include_china), value=preset), *apply_preset(preset, include_china))
|
| 417 |
+
|
| 418 |
+
|
| 419 |
def on_modules(modules: list[str]):
|
| 420 |
if CATALOG is None:
|
| 421 |
return gr.update(), gr.update()
|
|
|
|
| 532 |
files,
|
| 533 |
dimensions,
|
| 534 |
max_samples,
|
| 535 |
+
per_task,
|
| 536 |
concurrency,
|
| 537 |
prices,
|
| 538 |
) -> Plan:
|
| 539 |
+
slugs = {
|
| 540 |
+
"target": str(model_id).strip(),
|
| 541 |
+
"judge": "" if use_system_one_judge else str(judge_model_id).strip(),
|
| 542 |
+
"student": str(multiturn_model_id or "").strip() if use_multiturn_chat else "",
|
| 543 |
+
}
|
| 544 |
+
routes = {role: resolve_route(slug) for role, slug in slugs.items() if slug}
|
| 545 |
+
prompt_price, completion_price = _routed_price(routes.get("target"), _price_pair(prices, slugs["target"]))
|
| 546 |
+
judge_prompt_price, judge_completion_price = _routed_price(
|
| 547 |
+
routes.get("judge"), _price_pair(prices, str(judge_model_id).strip())
|
| 548 |
+
)
|
| 549 |
+
mt_prompt_price, mt_completion_price = _routed_price(
|
| 550 |
+
routes.get("student"), _price_pair(prices, str(multiturn_model_id or "").strip())
|
| 551 |
+
)
|
| 552 |
return build_plan(
|
| 553 |
CATALOG,
|
| 554 |
openrouter_model=str(model_id).strip(),
|
|
|
|
| 556 |
source_files=set(files or []),
|
| 557 |
dimensions=set(dimensions or []),
|
| 558 |
max_samples=_max_samples(max_samples),
|
| 559 |
+
max_samples_per_file=_max_samples(per_task),
|
| 560 |
prompt_usd_per_token=prompt_price,
|
| 561 |
completion_usd_per_token=completion_price,
|
| 562 |
max_concurrency=int(concurrency),
|
|
|
|
| 568 |
multiturn_openrouter_model=str(multiturn_model_id or "").strip() if use_multiturn_chat else None,
|
| 569 |
multiturn_prompt_usd_per_token=mt_prompt_price,
|
| 570 |
multiturn_completion_usd_per_token=mt_completion_price,
|
| 571 |
+
routes=routes,
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
def _plan_board(plan: Plan) -> str | None:
|
| 576 |
+
"""The judge board this plan's run will be ranked on."""
|
| 577 |
+
if not (plan.judge_calls or plan.basic_tasks) or plan.judge_model_config is None:
|
| 578 |
+
return None
|
| 579 |
+
return judge_board(
|
| 580 |
+
{"judge_model": plan.judge_model_config.model_name, "use_system_one_judge": plan.use_system_one_judge}
|
| 581 |
)
|
| 582 |
|
| 583 |
|
|
|
|
| 616 |
questions = plan.standard_requests + plan.basic_tasks
|
| 617 |
lines.append(f"### {questions:,} questions for `{plan.protocol.openrouter_model}`")
|
| 618 |
graded = plan.judge_calls + plan.basic_tasks
|
| 619 |
+
if graded and plan.judge_model_config is not None and graded >= questions:
|
| 620 |
+
lines.append(f"Every answer is graded by `{plan.judge_model_config.model_name}`.")
|
| 621 |
+
elif graded and plan.judge_model_config is not None:
|
| 622 |
lines.append(
|
| 623 |
f"{graded:,} answers are graded by `{plan.judge_model_config.model_name}`; "
|
| 624 |
"the rest are checked automatically against the answer key."
|
| 625 |
)
|
| 626 |
else:
|
| 627 |
lines.append("Every answer is checked automatically against the answer key.")
|
| 628 |
+
board = _plan_board(plan)
|
| 629 |
+
if board is not None and board != PAPER_JUDGE_SLUG:
|
| 630 |
+
lines.append("")
|
| 631 |
+
lines.append(
|
| 632 |
+
f"**Your own leaderboard:** results graded by `{plan.judge_model_config.model_name}` are ranked with "
|
| 633 |
+
"other runs that used the same judge (pick it under *Graded by* on the chart), not with the published "
|
| 634 |
+
f"results. Keep the default judge, {PAPER_JUDGE_LABEL}, to be ranked next to them."
|
| 635 |
+
)
|
| 636 |
lines.append("")
|
| 637 |
|
| 638 |
if plan.low_usd is None or plan.high_usd is None:
|
|
|
|
| 645 |
f"**Estimated cost: {_usd(plan.low_usd)} to {_usd(plan.high_usd)}.** The low end counts the "
|
| 646 |
"questions only; the high end assumes every answer runs to the length limit, so real runs "
|
| 647 |
"land well below it."
|
| 648 |
+
+ (
|
| 649 |
+
f" The judge has no length limit, so its high end assumes {JUDGE_HIGH_OUTPUT_TOKENS:,} tokens "
|
| 650 |
+
"per grade, thinking included."
|
| 651 |
+
if "judge" in plan.component_usd and not plan.use_system_one_judge
|
| 652 |
+
else ""
|
| 653 |
+
)
|
| 654 |
)
|
| 655 |
for role in ("target", "judge"):
|
| 656 |
if role in plan.component_usd:
|
|
|
|
| 667 |
)
|
| 668 |
lines.append("")
|
| 669 |
|
| 670 |
+
if plan.routes:
|
| 671 |
+
lines.append(
|
| 672 |
+
"**Hosts:** each model runs at the highest precision any OpenRouter host offers, on the cheapest "
|
| 673 |
+
"such host. If that host fails, the next cheapest at the same precision takes over."
|
| 674 |
+
)
|
| 675 |
+
names = {"target": "Model under test", "judge": "Judge", "student": "Simulated student"}
|
| 676 |
+
lines.extend(f"- {names[role]}: {route.describe()}" for role, route in plan.routes.items())
|
| 677 |
+
lines.append("")
|
| 678 |
+
|
| 679 |
problems = budget_problems(plan, total_budget, role_budgets)
|
| 680 |
if problems:
|
| 681 |
lines.append("**Spending limit too low:**")
|
|
|
|
| 695 |
)
|
| 696 |
lines.append("")
|
| 697 |
|
| 698 |
+
if plan.covers_core:
|
| 699 |
+
lines.append("This is a complete run: once it finishes, it appears on the leaderboard chart next to the published models.")
|
| 700 |
+
if not plan.is_full_benchmark:
|
| 701 |
+
lines.append(
|
| 702 |
+
"It leaves out the two China-specific sets, so it is scored like the chart's default view. "
|
| 703 |
+
"Tick *Include the China-specific sets* to also appear in the paper's original scoring."
|
| 704 |
+
)
|
| 705 |
else:
|
| 706 |
lines.append(
|
| 707 |
"This is a partial run. Its results are saved with the visitor runs, but the chart only shows "
|
|
|
|
| 732 |
files,
|
| 733 |
dimensions,
|
| 734 |
max_samples,
|
| 735 |
+
per_task,
|
| 736 |
concurrency,
|
| 737 |
prices,
|
| 738 |
total_budget,
|
|
|
|
| 764 |
files=files,
|
| 765 |
dimensions=dimensions,
|
| 766 |
max_samples=max_samples,
|
| 767 |
+
per_task=per_task,
|
| 768 |
concurrency=concurrency,
|
| 769 |
prices=prices,
|
| 770 |
)
|
|
|
|
| 817 |
files,
|
| 818 |
dimensions,
|
| 819 |
max_samples,
|
| 820 |
+
per_task,
|
| 821 |
concurrency,
|
| 822 |
prices,
|
| 823 |
total_budget,
|
|
|
|
| 871 |
files=files,
|
| 872 |
dimensions=dimensions,
|
| 873 |
max_samples=sample_cap,
|
| 874 |
+
per_task=per_task,
|
| 875 |
concurrency=worker_concurrency,
|
| 876 |
prices=prices,
|
| 877 |
)
|
|
|
|
| 968 |
lines = [heading, "", body]
|
| 969 |
if scores:
|
| 970 |
lines += ["", f"Scores on the questions that ran: {scores}"]
|
| 971 |
+
if not (result.get("leaderboard_record") or {}).get("covers_core"):
|
| 972 |
+
lines[-1] += ". A partial run is too small to compare with the leaderboard."
|
| 973 |
if persist_to_leaderboard:
|
| 974 |
lines += ["", "Saved to the visitor-run leaderboard."]
|
| 975 |
+
record = result.get("leaderboard_record") or {}
|
| 976 |
+
if status_label == "completed" and record.get("covers_core"):
|
| 977 |
+
board = judge_board(record)
|
| 978 |
+
where = (
|
| 979 |
+
"the leaderboard chart"
|
| 980 |
+
if board in (None, PAPER_JUDGE_SLUG)
|
| 981 |
+
else f"the leaderboard chart under *Graded by {judge_board_label(board)}*"
|
| 982 |
+
)
|
| 983 |
+
lines[-1] += f" It now appears on {where}; press **Refresh** there to see it."
|
| 984 |
else:
|
| 985 |
lines += ["", "Not saved to the leaderboard (you opted out)."]
|
| 986 |
lines += ["", f"<sub>Run id `{result.get('run_id')}`</sub>"]
|
|
|
|
| 1004 |
max_price,
|
| 1005 |
max_size,
|
| 1006 |
include_undisclosed,
|
| 1007 |
+
judge,
|
| 1008 |
+
include_china,
|
| 1009 |
prices,
|
| 1010 |
):
|
| 1011 |
runs = LEADERBOARD_STORE.load_runs()
|
|
|
|
| 1026 |
cost_metric=cost_metric or "blended_per_1m",
|
| 1027 |
include_custom_runs=bool(include_visitor_runs),
|
| 1028 |
keep=keep,
|
| 1029 |
+
scope=_scope(include_china),
|
| 1030 |
+
judge=judge or PAPER_JUDGE_SLUG,
|
| 1031 |
+
)
|
| 1032 |
+
return fig, rows, f"{md}\n\n<sub>{_scope_note(include_china)} {label}</sub>"
|
| 1033 |
+
|
| 1034 |
+
|
| 1035 |
+
def _scope_note(include_china: bool) -> str:
|
| 1036 |
+
if include_china:
|
| 1037 |
+
return (
|
| 1038 |
+
f"Paper's original scoring: all {scope_item_count(SCOPE_PAPER):,} questions, including the two "
|
| 1039 |
+
"China-specific sets."
|
| 1040 |
+
)
|
| 1041 |
+
return (
|
| 1042 |
+
f"Scored on {scope_item_count(SCOPE_CORE):,} questions, without the two China-specific sets "
|
| 1043 |
+
"(see About). Published 95% ranges cover the paper's original scoring only, so none are shown here."
|
| 1044 |
+
)
|
| 1045 |
+
|
| 1046 |
+
|
| 1047 |
+
def scope_tables(include_china, official_model):
|
| 1048 |
+
scope = _scope(include_china)
|
| 1049 |
+
runs = LEADERBOARD_STORE.load_runs()
|
| 1050 |
+
return (
|
| 1051 |
+
official_leaderboard_rows(scope),
|
| 1052 |
+
official_per_file_rows(official_model, scope),
|
| 1053 |
+
custom_leaderboard_rows(runs, scope),
|
| 1054 |
)
|
|
|
|
| 1055 |
|
| 1056 |
|
| 1057 |
def refresh_leaderboards(
|
|
|
|
| 1062 |
max_price,
|
| 1063 |
max_size,
|
| 1064 |
include_undisclosed,
|
| 1065 |
+
judge,
|
| 1066 |
+
include_china,
|
| 1067 |
primary_sel,
|
| 1068 |
baseline_sels,
|
| 1069 |
granularity,
|
| 1070 |
prices,
|
| 1071 |
):
|
| 1072 |
runs = LEADERBOARD_STORE.sync_from_hub()
|
| 1073 |
+
scope = _scope(include_china)
|
| 1074 |
fig, rows, md = leaderboard_view(
|
| 1075 |
+
focus,
|
| 1076 |
+
single_task,
|
| 1077 |
+
cost_metric,
|
| 1078 |
+
include_visitor_runs,
|
| 1079 |
+
max_price,
|
| 1080 |
+
max_size,
|
| 1081 |
+
include_undisclosed,
|
| 1082 |
+
judge,
|
| 1083 |
+
include_china,
|
| 1084 |
+
prices,
|
| 1085 |
)
|
| 1086 |
+
primary_choices, baseline_choices = build_comparison_choices(runs, scope)
|
| 1087 |
keys = [value for _, value in primary_choices]
|
| 1088 |
resolved_primary = primary_sel if primary_sel in keys else (keys[0] if keys else None)
|
| 1089 |
resolved_baselines = [b for b in (baseline_sels or []) if b in keys] or [
|
| 1090 |
key for key in ("official:deepseek-v4-flash", "official:gpt-5.4") if key in keys and key != resolved_primary
|
| 1091 |
]
|
| 1092 |
+
headers, comp_rows, comp_note = build_comparison_table(
|
| 1093 |
+
resolved_primary, resolved_baselines, granularity, runs, scope
|
| 1094 |
+
)
|
| 1095 |
return (
|
| 1096 |
fig,
|
| 1097 |
rows,
|
| 1098 |
md,
|
| 1099 |
+
custom_leaderboard_rows(runs, scope),
|
| 1100 |
f"{len(runs)} visitor run(s). {LEADERBOARD_STORE.sync_status}",
|
| 1101 |
+
_judge_board_update(runs, judge),
|
| 1102 |
gr.update(choices=primary_choices, value=resolved_primary),
|
| 1103 |
gr.update(choices=baseline_choices, value=resolved_baselines),
|
| 1104 |
gr.Dataframe(headers=headers, value=comp_rows, wrap=True),
|
|
|
|
| 1106 |
)
|
| 1107 |
|
| 1108 |
|
| 1109 |
+
def _judge_board_update(runs: list[dict], judge: str | None):
|
| 1110 |
+
choices = judge_board_choices(runs)
|
| 1111 |
+
keys = [key for _, key in choices]
|
| 1112 |
+
return gr.update(choices=choices, value=judge if judge in keys else PAPER_JUDGE_SLUG)
|
| 1113 |
+
|
| 1114 |
+
|
| 1115 |
+
def rescope_leaderboards(
|
| 1116 |
+
focus,
|
| 1117 |
+
single_task,
|
| 1118 |
+
cost_metric,
|
| 1119 |
+
include_visitor_runs,
|
| 1120 |
+
max_price,
|
| 1121 |
+
max_size,
|
| 1122 |
+
include_undisclosed,
|
| 1123 |
+
judge,
|
| 1124 |
+
include_china,
|
| 1125 |
+
primary_sel,
|
| 1126 |
+
baseline_sels,
|
| 1127 |
+
granularity,
|
| 1128 |
+
official_model,
|
| 1129 |
+
prices,
|
| 1130 |
+
):
|
| 1131 |
+
"""Switch every leaderboard view between the default scope and the paper's original scoring."""
|
| 1132 |
+
scope = _scope(include_china)
|
| 1133 |
+
runs = LEADERBOARD_STORE.load_runs()
|
| 1134 |
+
fig, rows, md = leaderboard_view(
|
| 1135 |
+
focus,
|
| 1136 |
+
single_task,
|
| 1137 |
+
cost_metric,
|
| 1138 |
+
include_visitor_runs,
|
| 1139 |
+
max_price,
|
| 1140 |
+
max_size,
|
| 1141 |
+
include_undisclosed,
|
| 1142 |
+
judge,
|
| 1143 |
+
include_china,
|
| 1144 |
+
prices,
|
| 1145 |
+
)
|
| 1146 |
+
primary_choices, baseline_choices = build_comparison_choices(runs, scope)
|
| 1147 |
+
headers, comp_rows, comp_note = build_comparison_table(primary_sel, baseline_sels, granularity, runs, scope)
|
| 1148 |
+
official_rows, official_files, visitor_rows = scope_tables(include_china, official_model)
|
| 1149 |
+
return (
|
| 1150 |
+
fig,
|
| 1151 |
+
rows,
|
| 1152 |
+
md,
|
| 1153 |
+
official_rows,
|
| 1154 |
+
official_files,
|
| 1155 |
+
visitor_rows,
|
| 1156 |
+
gr.update(choices=primary_choices, value=primary_sel),
|
| 1157 |
+
gr.update(choices=baseline_choices, value=baseline_sels),
|
| 1158 |
+
gr.Dataframe(headers=headers, value=comp_rows, wrap=True),
|
| 1159 |
+
comp_note,
|
| 1160 |
+
)
|
| 1161 |
+
|
| 1162 |
+
|
| 1163 |
+
def update_comparison(primary_sel, baseline_sels, granularity, include_china=False):
|
| 1164 |
headers, comp_rows, comp_note = build_comparison_table(
|
| 1165 |
+
primary_sel, baseline_sels, granularity, LEADERBOARD_STORE.load_runs(), _scope(include_china)
|
| 1166 |
)
|
| 1167 |
return gr.Dataframe(headers=headers, value=comp_rows, wrap=True), comp_note
|
| 1168 |
|
|
|
|
| 1170 |
def _about_markdown() -> str:
|
| 1171 |
counts: dict[str, int] = {}
|
| 1172 |
if CATALOG is not None:
|
| 1173 |
+
for entry in _in_scope(list(CATALOG.files), False):
|
| 1174 |
counts[entry.module] = counts.get(entry.module, 0) + entry.item_count
|
| 1175 |
rows = "\n".join(
|
| 1176 |
f"| **{label}** | {counts.get(module, 0):,} | {description} |"
|
|
|
|
| 1186 |
|:-----|:---------:|:---------------|
|
| 1187 |
{rows}
|
| 1188 |
|
| 1189 |
+
## Two China-specific sets, left out by default
|
| 1190 |
+
|
| 1191 |
+
ELBench was built in China. Two of its sets test agreement with China's national rules, not anything about
|
| 1192 |
+
education, so every score, the chart and the run settings leave them out unless you tick
|
| 1193 |
+
*Include the China-specific sets*:
|
| 1194 |
+
|
| 1195 |
+
- **Refusal under China's content rules** (250 safety questions). The model is expected to refuse, and 212 of
|
| 1196 |
+
the questions are filed under China's rules on "socialist core values": criticism of the government, the
|
| 1197 |
+
2022 White Paper protests, human rights. A model that answers openly is marked unsafe. In the published
|
| 1198 |
+
results GPT-5.4, Gemini-3.5-Flash and Claude-Opus-4.8 pass 32–40% of them; GLM-5.1 and DeepSeek-V4-Flash
|
| 1199 |
+
pass 96–99%.
|
| 1200 |
+
- **C-Eval** (208 knowledge questions) from Chinese national exams, including Marxism, Mao Zedong Thought and
|
| 1201 |
+
political education.
|
| 1202 |
+
|
| 1203 |
+
Without them the benchmark has {scope_item_count(SCOPE_CORE):,} questions. With them it has all
|
| 1204 |
+
{scope_item_count(SCOPE_PAPER):,}, scored exactly as in the paper. The published results are given per task,
|
| 1205 |
+
so a whole task is the smallest part that can be removed while still re-scoring the published models. A few
|
| 1206 |
+
questions elsewhere touch Chinese politics, for example about 3 of the 250 safety-guidance prompts, and stay in.
|
| 1207 |
+
|
| 1208 |
## How the scores work
|
| 1209 |
|
| 1210 |
- Each area is scored from 0 to 100. **Overall** is the average of the four areas.
|
| 1211 |
- Questions with a single right answer are checked automatically. Open-ended answers (safety guidance,
|
| 1212 |
teaching, character and thinking skills) are graded by a judge model against a rubric.
|
| 1213 |
+
- The published results include a 95% range from 10,000 bootstrap resamples, for the paper's original scoring
|
| 1214 |
+
only. When two models' ranges overlap, the difference between them could disappear on a re-run, and the
|
| 1215 |
+
chart says so.
|
| 1216 |
- **Best value** models score higher than every model that costs less. Price is the OpenRouter price per
|
| 1217 |
million tokens, weighting input three times as heavily as output, a common way to compare models.
|
| 1218 |
|
| 1219 |
## Visitor runs
|
| 1220 |
|
| 1221 |
- Runs use the same questions, prompts, retries and scoring code as the paper.
|
| 1222 |
+
- Models are called through OpenRouter. Each one runs at the highest precision any of its hosts offers
|
| 1223 |
+
(for example fp8 rather than fp4), on the cheapest such host. Models whose hosts don't say, such as most
|
| 1224 |
+
closed models, go to the cheapest host.
|
| 1225 |
- The default judge is Qwen3-235B on OpenRouter. The paper used its own hosted Qwen judge, so judged scores
|
| 1226 |
+
from visitor runs can differ from the published ones.
|
| 1227 |
+
- Judges disagree: the same answers can score very differently under two judge models. So each judge gets
|
| 1228 |
+
its own leaderboard (*Graded by* on the chart), and only runs graded by the default judge are ranked with
|
| 1229 |
+
the published results. Questions with a single right answer don't need a judge, so those scores carry
|
| 1230 |
+
across every board.
|
| 1231 |
- Only runs that answered every question in an area appear on the chart for that area, so partial runs never
|
| 1232 |
compete with complete ones.
|
| 1233 |
|
|
|
|
| 1294 |
initial_runs = LEADERBOARD_STORE.load_runs()
|
| 1295 |
initial_prices, _ = price_snapshot()
|
| 1296 |
init_fig, init_rows, init_md = leaderboard_view(
|
| 1297 |
+
"overall", "", "blended_per_1m", True, PRICE_FILTER_MAX, SIZE_FILTER_MAX, True, PAPER_JUDGE_SLUG, False,
|
| 1298 |
+
initial_prices,
|
| 1299 |
)
|
| 1300 |
+
primary_choices, baseline_choices = build_comparison_choices(initial_runs, SCOPE_CORE)
|
| 1301 |
default_primary = primary_choices[0][1] if primary_choices else "official:gpt-5.4"
|
| 1302 |
default_baselines = [k for k in ("official:deepseek-v4-flash", "official:gpt-5.4") if k != default_primary]
|
| 1303 |
granularity_default = "By Source File / Task (Matched Subset)"
|
| 1304 |
init_comp_headers, init_comp_rows, init_comp_note = build_comparison_table(
|
| 1305 |
+
default_primary, default_baselines, granularity_default, initial_runs, SCOPE_CORE
|
| 1306 |
)
|
| 1307 |
+
default_files = _preset_files(PRESET_QUICK, False)
|
| 1308 |
|
| 1309 |
with gr.Blocks(title="ELBench-live") as demo:
|
| 1310 |
prices = gr.State(initial_prices)
|
|
|
|
| 1313 |
with gr.Tab("Leaderboard", id="leaderboard"):
|
| 1314 |
gr.Markdown(
|
| 1315 |
"# How well do AI models handle education?\n"
|
| 1316 |
+
f"ELBench scores AI models on {scope_item_count(SCOPE_CORE):,} questions in four areas: "
|
| 1317 |
+
"**teaching**, **safety**, **knowledge & reasoning**, and **character & thinking skills**. "
|
| 1318 |
+
"Each dot is a model: higher scores better, further left costs less."
|
| 1319 |
)
|
| 1320 |
focus = gr.Radio(label="Area", choices=FOCUS_CHOICES, value="overall")
|
| 1321 |
+
include_china = gr.Checkbox(
|
| 1322 |
+
label="Include the China-specific sets (the paper's original scoring)",
|
| 1323 |
+
value=False,
|
| 1324 |
+
info="Adds two sets that test China's content rules and national exams. See About.",
|
| 1325 |
+
)
|
| 1326 |
+
judge_board_pick = gr.Dropdown(
|
| 1327 |
+
label="Graded by",
|
| 1328 |
+
choices=judge_board_choices(initial_runs),
|
| 1329 |
+
value=PAPER_JUDGE_SLUG,
|
| 1330 |
+
info="Each judge model has its own leaderboard. The published results were graded by the "
|
| 1331 |
+
"paper's judge, so they appear on the default one.",
|
| 1332 |
+
)
|
| 1333 |
chart_max_price, chart_max_size, chart_undisclosed = _filter_controls()
|
| 1334 |
chart = gr.Plot(value=init_fig, show_label=False)
|
| 1335 |
takeaway = gr.Markdown(value=init_md)
|
|
|
|
| 1359 |
show_label=False,
|
| 1360 |
)
|
| 1361 |
|
| 1362 |
+
with gr.Accordion("Published results", open=False):
|
| 1363 |
+
official_table = gr.Dataframe(
|
| 1364 |
headers=[
|
| 1365 |
"Rank",
|
| 1366 |
"Model",
|
|
|
|
| 1371 |
"Character & thinking",
|
| 1372 |
"Questions",
|
| 1373 |
],
|
| 1374 |
+
value=official_leaderboard_rows(SCOPE_CORE),
|
| 1375 |
wrap=True,
|
| 1376 |
show_label=False,
|
| 1377 |
)
|
|
|
|
| 1383 |
)
|
| 1384 |
official_file_table = gr.Dataframe(
|
| 1385 |
headers=["Model", "Area", "Task", "Questions", "Score", "Pass rate (%)", "Mean raw score"],
|
| 1386 |
+
value=official_per_file_rows("GPT-5.4", SCOPE_CORE),
|
| 1387 |
wrap=True,
|
| 1388 |
show_label=False,
|
| 1389 |
)
|
|
|
|
| 1406 |
"Teaching",
|
| 1407 |
"Character & thinking",
|
| 1408 |
],
|
| 1409 |
+
value=custom_leaderboard_rows(initial_runs, SCOPE_CORE),
|
| 1410 |
wrap=True,
|
| 1411 |
show_label=False,
|
| 1412 |
)
|
|
|
|
| 1460 |
with gr.Accordion("Narrow the list by price or size", open=False):
|
| 1461 |
pick_max_price, pick_max_size, pick_undisclosed = _filter_controls()
|
| 1462 |
models_shown = gr.Markdown()
|
| 1463 |
+
preset = gr.Radio(label="How much to run", choices=_preset_choices(False), value=PRESET_QUICK)
|
| 1464 |
+
run_include_china = gr.Checkbox(
|
| 1465 |
+
label="Include the China-specific sets",
|
| 1466 |
+
value=False,
|
| 1467 |
+
info="Needed only to appear in the paper's original scoring. See About.",
|
| 1468 |
+
)
|
| 1469 |
with gr.Row():
|
| 1470 |
total_budget = gr.Number(
|
| 1471 |
label="Spending limit (USD)",
|
|
|
|
| 1488 |
value=DEFAULT_CHAT_JUDGE_SLUG,
|
| 1489 |
allow_custom_value=True,
|
| 1490 |
filterable=True,
|
| 1491 |
+
info="Grades open-ended answers. The default is the closest OpenRouter match to the paper's "
|
| 1492 |
+
"judge; runs graded by another judge get their own leaderboard.",
|
| 1493 |
)
|
| 1494 |
use_system_one_judge = gr.Checkbox(
|
| 1495 |
label="Use a decision model as the judge",
|
|
|
|
| 1520 |
modules = gr.CheckboxGroup(
|
| 1521 |
label="Areas",
|
| 1522 |
choices=_module_choices(),
|
| 1523 |
+
value=list(dict.fromkeys(entry.module for entry in default_files)),
|
| 1524 |
info="Choosing an area selects all of its tasks.",
|
| 1525 |
)
|
| 1526 |
files = gr.CheckboxGroup(
|
|
|
|
| 1534 |
value=[],
|
| 1535 |
)
|
| 1536 |
with gr.Row():
|
| 1537 |
+
per_task = gr.Number(
|
| 1538 |
+
label="Questions per task",
|
| 1539 |
+
value=QUICK_CHECK_PER_TASK,
|
| 1540 |
+
precision=0,
|
| 1541 |
+
info="Takes the first questions of each task. Leave empty for all of them.",
|
| 1542 |
+
)
|
| 1543 |
max_samples = gr.Number(
|
| 1544 |
label="Stop after this many questions",
|
| 1545 |
+
value=None,
|
| 1546 |
precision=0,
|
| 1547 |
info="Leave empty to run every selected question.",
|
| 1548 |
)
|
|
|
|
| 1587 |
chart_max_price,
|
| 1588 |
chart_max_size,
|
| 1589 |
chart_undisclosed,
|
| 1590 |
+
judge_board_pick,
|
| 1591 |
+
include_china,
|
| 1592 |
]
|
| 1593 |
view_outputs = [chart, value_table, takeaway]
|
| 1594 |
+
for control in view_controls[:-1]:
|
| 1595 |
control.change(leaderboard_view, view_controls + [prices], view_outputs)
|
| 1596 |
+
include_china.change(
|
| 1597 |
+
rescope_leaderboards,
|
| 1598 |
+
view_controls + [comp_primary, comp_baselines, comp_granularity, official_model_filter, prices],
|
| 1599 |
+
view_outputs
|
| 1600 |
+
+ [official_table, official_file_table, runs_table, comp_primary, comp_baselines, comp_table, comp_note],
|
| 1601 |
+
)
|
| 1602 |
refresh.click(
|
| 1603 |
refresh_leaderboards,
|
| 1604 |
view_controls + [comp_primary, comp_baselines, comp_granularity, prices],
|
| 1605 |
+
[
|
| 1606 |
+
chart,
|
| 1607 |
+
value_table,
|
| 1608 |
+
takeaway,
|
| 1609 |
+
runs_table,
|
| 1610 |
+
runs_status,
|
| 1611 |
+
judge_board_pick,
|
| 1612 |
+
comp_primary,
|
| 1613 |
+
comp_baselines,
|
| 1614 |
+
comp_table,
|
| 1615 |
+
comp_note,
|
| 1616 |
+
],
|
| 1617 |
)
|
| 1618 |
go_test.click(lambda: gr.Tabs(selected="test"), None, tabs)
|
| 1619 |
+
official_model_filter.change(
|
| 1620 |
+
lambda model_name, china: official_per_file_rows(model_name, _scope(china)),
|
| 1621 |
+
[official_model_filter, include_china],
|
| 1622 |
+
[official_file_table],
|
| 1623 |
+
)
|
| 1624 |
for control in (comp_primary, comp_baselines, comp_granularity):
|
| 1625 |
+
control.change(
|
| 1626 |
+
update_comparison,
|
| 1627 |
+
[comp_primary, comp_baselines, comp_granularity, include_china],
|
| 1628 |
+
[comp_table, comp_note],
|
| 1629 |
+
)
|
| 1630 |
|
| 1631 |
# Test-a-model events
|
| 1632 |
pick_filters = [pick_max_price, pick_max_size, pick_undisclosed]
|
|
|
|
| 1639 |
control.change(filter_target_models, [model] + pick_filters, [model, models_shown])
|
| 1640 |
use_system_one_judge.change(on_toggle_system_one, [use_system_one_judge], [judge_model])
|
| 1641 |
use_multiturn_chat.change(on_toggle_multiturn, [use_multiturn_chat], [multiturn_model])
|
| 1642 |
+
preset.change(apply_preset, [preset, run_include_china], [modules, files, dimensions, max_samples, per_task])
|
| 1643 |
+
run_include_china.change(
|
| 1644 |
+
on_include_china,
|
| 1645 |
+
[preset, run_include_china],
|
| 1646 |
+
[preset, modules, files, dimensions, max_samples, per_task],
|
| 1647 |
+
)
|
| 1648 |
modules.input(on_modules, [modules], [files, dimensions])
|
| 1649 |
files.input(on_files, [files], [dimensions])
|
| 1650 |
run_inputs = [
|
|
|
|
| 1658 |
files,
|
| 1659 |
dimensions,
|
| 1660 |
max_samples,
|
| 1661 |
+
per_task,
|
| 1662 |
concurrency,
|
| 1663 |
prices,
|
| 1664 |
total_budget,
|
docs/DASHBOARD_TARGETS.md
CHANGED
|
@@ -179,6 +179,149 @@ know it is done. Work them in order; update **Status** as each one lands.
|
|
| 179 |
The same three controls (highest price, largest size, include undisclosed) sit above the chart and in the
|
| 180 |
model picker. Filtered models leave the chart, table and best-value line, with a note saying how many.
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
## Testing
|
| 183 |
|
| 184 |
- Unit tests in `tests/test_dashboard_faithfulness.py` per target.
|
|
|
|
| 179 |
The same three controls (highest price, largest size, include undisclosed) sit above the chart and in the
|
| 180 |
model picker. Filtered models leave the chart, table and best-value line, with a note saying how many.
|
| 181 |
|
| 182 |
+
## Phase 4 · What the score measures
|
| 183 |
+
|
| 184 |
+
- **D4 China-specific sets are opt-in.** Two files are keyed to China's national rules, not to
|
| 185 |
+
education: the general "should refuse" set and C-Eval. Every score, chart and default run leaves
|
| 186 |
+
them out. The paper's full scope stays one tick away and reproduces the published table. This is
|
| 187 |
+
the documented exception to D1.
|
| 188 |
+
|
| 189 |
+
### T15 Default scope without the China-specific sets
|
| 190 |
+
- **Issue:** the paper's Safety and Knowledge scores include material that rewards following China's
|
| 191 |
+
content rules, so a model that discusses Chinese politics openly scores as unsafe.
|
| 192 |
+
- **Evidence:**
|
| 193 |
+
- **Should-refuse set (`安全拒答.jsonl`, 250):** 212 items are labelled A.1 ("socialist core values":
|
| 194 |
+
state power, national image, unity). Most of the other 38 are also framed around the Chinese state:
|
| 195 |
+
criticism of its privacy record, its pressure on Trump, East Turkestan, regional discrimination
|
| 196 |
+
within China.
|
| 197 |
+
- **Published pass rates on that set:** GPT-5.4 31.6%, Gemini-3.5-Flash 35.2%, Claude-Opus-4.8
|
| 198 |
+
39.6%, against GLM-5.1 98.8%, DeepSeek-V4-Flash 96.4% and Safe-InnoSpark 99.6%.
|
| 199 |
+
- **C-Eval (`ceval_sampled.jsonl`, 208):** Chinese national exams, 4 items per subject. 24 of them
|
| 200 |
+
are political subjects: Marxism, Mao Zedong Thought, middle- and high-school politics,
|
| 201 |
+
ideological and moral cultivation, modern Chinese history.
|
| 202 |
+
- The results dataset publishes per-file scores, not per-item ones. So a whole file can be dropped
|
| 203 |
+
and the published models re-scored, but the 24 political C-Eval items can't be removed on
|
| 204 |
+
their own. C-Eval is excluded whole for that reason.
|
| 205 |
+
- **Kept, with notes:**
|
| 206 |
+
- **Should-answer set:** its 45 A.1 items are harmless questions about Chinese culture and
|
| 207 |
+
technology (Dunhuang murals, the EAST reactor), which test over-refusal.
|
| 208 |
+
- **Guided-answer set:** unlabelled. Its political items are mostly universal harms (defamation
|
| 209 |
+
campaigns, a teacher steering students' politics). About 3 of 250 are state-centric ("weaken
|
| 210 |
+
students' national identity", separatism in class meetings).
|
| 211 |
+
- **Character & thinking skills (omni):** 8 of 500 touch politics, none partisan.
|
| 212 |
+
- **Question-generation scenario:** one rubric dimension cites "socialist core values", but its
|
| 213 |
+
criteria are universal (honesty, environment, diligence, friendliness, obeying the law).
|
| 214 |
+
- **Resolution:** a core scope (2,481 questions) that drops the two files, used by default for the
|
| 215 |
+
chart, both tables, the comparison and the run presets. A checkbox switches to the paper's full
|
| 216 |
+
scope (2,939). The published 95% ranges exist only for the full scope; the core view says so.
|
| 217 |
+
- **Done when:**
|
| 218 |
+
- recomputing every published model's full scope from per-file data still matches the paper;
|
| 219 |
+
- core scores come from the same function;
|
| 220 |
+
- a run without the two files is complete in the core view and plotted there;
|
| 221 |
+
- the About tab explains the split.
|
| 222 |
+
- **Status:** done 2026-10-26.
|
| 223 |
+
- `CHINA_SPECIFIC_FILES`, `scope_files`, `scores_in_scope` and `official_scores` in `leaderboard.py`.
|
| 224 |
+
The chart, both tables, the per-task table and the comparison take a `scope`.
|
| 225 |
+
- A checkbox above the chart switches to the paper's scoring, and one on the run tab adds the two
|
| 226 |
+
sets to a run. Both files are relabelled ("Refusal under China's content rules", "C-Eval, Chinese
|
| 227 |
+
national exams"), and the About tab has a section on them.
|
| 228 |
+
- Core ranking: GPT-5.4 86.3, Gemini-3.5-Flash 85.6, Claude-Opus-4.8 85.3, Doubao 84.7,
|
| 229 |
+
DeepSeek-V4-Flash 82.8 (1st under the paper's scoring).
|
| 230 |
+
- Tests: `test_core_scope_drops_the_china_specific_sets_and_keeps_paper_scope_reproducible`,
|
| 231 |
+
`test_run_without_china_specific_sets_is_complete_and_plotted_in_core_scope_only`,
|
| 232 |
+
`test_plan_reports_core_coverage`.
|
| 233 |
+
|
| 234 |
+
### T16 Quick check samples every task
|
| 235 |
+
- **Issue:** the Quick check takes the first 5 questions in file order, so it tested one kind of
|
| 236 |
+
question (e.g. 5 political refusal items) and none of the judge or teaching paths.
|
| 237 |
+
- **Resolution:**
|
| 238 |
+
- a per-task limit in the runner (`max_samples_per_file`), honoured by the standard and teaching
|
| 239 |
+
branches, the cost estimate and the run fingerprint;
|
| 240 |
+
- the Quick check asks one question per task in the chosen scope.
|
| 241 |
+
- **Done when:** a Quick check plan lists one item per selected file, and a live check runs one
|
| 242 |
+
question per task.
|
| 243 |
+
- **Status:** done 2026-10-26.
|
| 244 |
+
- The Quick check is 16 questions in the default scope (18 with the China-specific sets).
|
| 245 |
+
- Live check (GPT-6 Luna, GLM 5.3 Flash judge, local store, ~$0.02) ran 12 standard questions and
|
| 246 |
+
4 teaching tasks. It surfaced the two runtime faults fixed in T18.
|
| 247 |
+
- Test: `test_quick_check_takes_one_question_from_each_task`.
|
| 248 |
+
|
| 249 |
+
### T17 Partial runs never show a headline number
|
| 250 |
+
- **Issue:** the visitor-run table showed "Overall 21.8" for a 5-question run.
|
| 251 |
+
- **Resolution:** a score appears in a visitor-run row only for areas the run covered in full, in
|
| 252 |
+
the scope being viewed. Otherwise the cell is blank, and the row still shows how many answers ran.
|
| 253 |
+
- **Done when:** a unit test shows a partial run with blank area and overall cells.
|
| 254 |
+
- **Status:** done 2026-10-26. `custom_leaderboard_rows(runs, scope)` and `run_coverage_label`.
|
| 255 |
+
Comparison labels say "partial" in place of an overall score. Test:
|
| 256 |
+
`test_partial_visitor_run_shows_no_headline_score`.
|
| 257 |
+
|
| 258 |
+
### T18 Any OpenRouter model works as judge or student
|
| 259 |
+
- **Issue:** two teaching tasks failed with GLM 5.3 Flash as judge ("Output does not contain
|
| 260 |
+
<START OUTPUT> and <END OUTPUT>"). With Qwen3-235B they passed.
|
| 261 |
+
- **Evidence:**
|
| 262 |
+
- All four teaching tasks use the same `format_mode: prompt` and the same wrapper instruction, so
|
| 263 |
+
the requested schema is not the difference.
|
| 264 |
+
- The dashboard capped the judge at `min(1024, …)` and the student at `min(512, …)`, and
|
| 265 |
+
`LLMJudge` capped again at 1024. The cap meant to turn thinking off
|
| 266 |
+
(`chat_template_kwargs.enable_thinking`) is a vLLM/Qwen setting that OpenRouter ignores.
|
| 267 |
+
- A reasoning judge used up the 1024 tokens thinking on the longer transcripts (knowledge
|
| 268 |
+
explanation, guided tutoring), so its grade never arrived.
|
| 269 |
+
- Separately, the runtime ran a bare `python`, which fails where only the venv interpreter or
|
| 270 |
+
`python3` exists.
|
| 271 |
+
- **Resolution:**
|
| 272 |
+
- No length limit is sent for visitor judge or student models. A limit set by a model card is still
|
| 273 |
+
used, so the paper's own judge card (`qwen-test`, 1024 / 512) is unchanged.
|
| 274 |
+
- The cost estimate assumes 8,192 judge output tokens at the high end and says so.
|
| 275 |
+
- `runtime_python: python` means `sys.executable`.
|
| 276 |
+
- **Done when:** the two tasks that failed pass with GLM 5.3 Flash as judge.
|
| 277 |
+
- **Status:** done 2026-10-26 (`test_visitor_judge_and_student_have_no_length_limit`). Live: knowledge
|
| 278 |
+
explanation and guided tutoring, both judged by GLM 5.3 Flash, completed with 0 errors ($0.004).
|
| 279 |
+
|
| 280 |
+
### T19 Each model runs at its highest precision, on the cheapest host
|
| 281 |
+
- **Issue:** requests carried no `provider` object, so OpenRouter could serve the same model from an
|
| 282 |
+
fp4 host on one question and an fp8 host on the next. GLM 5.3 Flash has 38 hosts: 11 fp8, 15 fp4 or
|
| 283 |
+
nvfp4, 12 unreported.
|
| 284 |
+
- **Resolution:** `routing.py` reads `GET /api/v1/models/{slug}/endpoints` (public, cached 15 min).
|
| 285 |
+
- It keeps the highest precision any host reports. Hosts that report none are dropped when others
|
| 286 |
+
do report.
|
| 287 |
+
- The request sends `provider: {sort: "price", quantizations: [...]}`, so fallbacks stay at that
|
| 288 |
+
precision.
|
| 289 |
+
- When no host reports a precision (most closed models), only `sort: "price"` is sent. Opt-in
|
| 290 |
+
`priority` tiers are ignored, and `:nitro` keeps its own speed sort.
|
| 291 |
+
- This applies to the target, the chat judge and the simulated student. The teaching runtime gets
|
| 292 |
+
it through LangChain's `extra_body`. Decision-model judges are not routed.
|
| 293 |
+
- The estimate prices each role at its cheapest routed host and lists the host choice. The run record
|
| 294 |
+
keeps it under `routing`. The run fingerprint is unchanged, so earlier runs still resume.
|
| 295 |
+
- **Done when:** a live request is served by a host at the chosen precision, and a run completes with
|
| 296 |
+
routing on every path.
|
| 297 |
+
- **Status:** done 2026-10-26.
|
| 298 |
+
- Live probe: Qwen3-235B (fp8) was served by DeepInfra (fp8) and GLM 5.3 Flash (fp8) by GMICloud
|
| 299 |
+
(fp8). GPT-6 Luna (unreported) went to OpenAI.
|
| 300 |
+
- Live run (GPT-6 Luna, GLM judge, one multiple-choice, one judged and one teaching question):
|
| 301 |
+
0 errors, $0.0015.
|
| 302 |
+
- Tests: `test_route_keeps_the_highest_precision_and_sorts_by_price`,
|
| 303 |
+
`test_plan_sends_the_route_with_every_chat_model`.
|
| 304 |
+
|
| 305 |
+
### T20 Each judge has its own leaderboard
|
| 306 |
+
- **Issue:** a visitor-chosen judge was ranked next to the published results, which the paper's judge
|
| 307 |
+
graded. Judges disagree a lot.
|
| 308 |
+
- On the same two teaching tasks, Qwen gave 45.0 and GLM gave 83.9. These were separate runs, so the
|
| 309 |
+
answers differed too.
|
| 310 |
+
- The paper's own judge-panel audit (`audit-judge-integrity/judge_panel_kappa.json`, n=80) finds
|
| 311 |
+
κ 0.025 and 0.125 against the primary judge, and Krippendorff's α −0.26 across the panel. That
|
| 312 |
+
file has no human gold set, although the README describes one.
|
| 313 |
+
- **Resolution:**
|
| 314 |
+
- A "Graded by" selector on the chart. The default board (Qwen3-235B) holds the published results
|
| 315 |
+
and the runs it graded. Every other judge, including each decision model, gets a board with only
|
| 316 |
+
its own runs.
|
| 317 |
+
- A run whose answers were all checked against the answer key (`judge_used: false`) appears on
|
| 318 |
+
every board.
|
| 319 |
+
- The comparison table warns when the results it compares were graded by different judges.
|
| 320 |
+
- The estimate, the finish message and the About tab say where a run will appear.
|
| 321 |
+
- **Done when:** the published results appear only on the default board, a GLM-judged run only on
|
| 322 |
+
its own board, and an answer-key-only run on all of them.
|
| 323 |
+
- **Status:** done 2026-10-26 (`test_each_judge_has_its_own_board`).
|
| 324 |
+
|
| 325 |
## Testing
|
| 326 |
|
| 327 |
- Unit tests in `tests/test_dashboard_faithfulness.py` per target.
|
src/elbench/dashboard/english.py
CHANGED
|
@@ -17,7 +17,7 @@ MODULE_ENGLISH_LABELS: dict[str, str] = {
|
|
| 17 |
}
|
| 18 |
|
| 19 |
SUBSET_ENGLISH_LABELS: dict[str, str] = {
|
| 20 |
-
"通用-应拒答": "General Safety:
|
| 21 |
"通用-引导回答": "General Safety: Constructive Safe Guidance",
|
| 22 |
"通用-应回答": "General Safety: Benign Request Answering",
|
| 23 |
"教育-教学安全": "Educational Teaching Safety",
|
|
@@ -37,7 +37,7 @@ SUBSET_ENGLISH_LABELS: dict[str, str] = {
|
|
| 37 |
}
|
| 38 |
|
| 39 |
FILE_ENGLISH_LABELS: dict[str, str] = {
|
| 40 |
-
"安全拒答.jsonl": "safety_refusal.jsonl (
|
| 41 |
"安全引导.jsonl": "safety_guidance.jsonl (Constructive Safe Guidance)",
|
| 42 |
"安全回答.jsonl": "safety_answer.jsonl (Benign Request Answering)",
|
| 43 |
"SATAs.xlsx": "SATAs.xlsx (Teaching Harm Identification)",
|
|
@@ -49,7 +49,7 @@ FILE_ENGLISH_LABELS: dict[str, str] = {
|
|
| 49 |
"cross.yaml": "cross.yaml (Interdisciplinary Lesson Plan)",
|
| 50 |
"config_guided_task.yaml": "config_guided_task.yaml (Multi-Turn Guided Tutoring)",
|
| 51 |
"mmlu_pro_sampled.jsonl": "mmlu_pro_sampled.jsonl (MMLU-Pro)",
|
| 52 |
-
"ceval_sampled.jsonl": "ceval_sampled.jsonl (C-Eval)",
|
| 53 |
"ifeval_sampled.jsonl": "ifeval_sampled.jsonl (IFEval)",
|
| 54 |
"math_500_sampled.jsonl": "math_500_sampled.jsonl (MATH-500)",
|
| 55 |
"aime24_sampled.jsonl": "aime24_sampled.jsonl (AIME 2024)",
|
|
@@ -58,7 +58,7 @@ FILE_ENGLISH_LABELS: dict[str, str] = {
|
|
| 58 |
}
|
| 59 |
|
| 60 |
TASK_ENGLISH_LABELS: dict[str, str] = {
|
| 61 |
-
"safety_refusal": "Safety:
|
| 62 |
"safety_guidance": "Safety: Constructive Safe Guidance",
|
| 63 |
"safety_answer": "Safety: Benign Request Answering",
|
| 64 |
"teaching_harm": "Safety: Teaching Harm (SATAs)",
|
|
@@ -140,7 +140,7 @@ def area_label(module: str) -> str:
|
|
| 140 |
|
| 141 |
|
| 142 |
def plain_file(canonical_name: str) -> str:
|
| 143 |
-
"""The readable part of a file label, without the file name: ``
|
| 144 |
label = english_file(canonical_name)
|
| 145 |
if "(" in label and label.endswith(")"):
|
| 146 |
return label[label.index("(") + 1 : -1]
|
|
|
|
| 17 |
}
|
| 18 |
|
| 19 |
SUBSET_ENGLISH_LABELS: dict[str, str] = {
|
| 20 |
+
"通用-应拒答": "General Safety: Refusal under China's content rules",
|
| 21 |
"通用-引导回答": "General Safety: Constructive Safe Guidance",
|
| 22 |
"通用-应回答": "General Safety: Benign Request Answering",
|
| 23 |
"教育-教学安全": "Educational Teaching Safety",
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
FILE_ENGLISH_LABELS: dict[str, str] = {
|
| 40 |
+
"安全拒答.jsonl": "safety_refusal.jsonl (Refusal under China's content rules)",
|
| 41 |
"安全引导.jsonl": "safety_guidance.jsonl (Constructive Safe Guidance)",
|
| 42 |
"安全回答.jsonl": "safety_answer.jsonl (Benign Request Answering)",
|
| 43 |
"SATAs.xlsx": "SATAs.xlsx (Teaching Harm Identification)",
|
|
|
|
| 49 |
"cross.yaml": "cross.yaml (Interdisciplinary Lesson Plan)",
|
| 50 |
"config_guided_task.yaml": "config_guided_task.yaml (Multi-Turn Guided Tutoring)",
|
| 51 |
"mmlu_pro_sampled.jsonl": "mmlu_pro_sampled.jsonl (MMLU-Pro)",
|
| 52 |
+
"ceval_sampled.jsonl": "ceval_sampled.jsonl (C-Eval, Chinese national exams)",
|
| 53 |
"ifeval_sampled.jsonl": "ifeval_sampled.jsonl (IFEval)",
|
| 54 |
"math_500_sampled.jsonl": "math_500_sampled.jsonl (MATH-500)",
|
| 55 |
"aime24_sampled.jsonl": "aime24_sampled.jsonl (AIME 2024)",
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
TASK_ENGLISH_LABELS: dict[str, str] = {
|
| 61 |
+
"safety_refusal": "Safety: Refusal under China's content rules",
|
| 62 |
"safety_guidance": "Safety: Constructive Safe Guidance",
|
| 63 |
"safety_answer": "Safety: Benign Request Answering",
|
| 64 |
"teaching_harm": "Safety: Teaching Harm (SATAs)",
|
|
|
|
| 140 |
|
| 141 |
|
| 142 |
def plain_file(canonical_name: str) -> str:
|
| 143 |
+
"""The readable part of a file label, without the file name: ``Benign Request Answering``."""
|
| 144 |
label = english_file(canonical_name)
|
| 145 |
if "(" in label and label.endswith(")"):
|
| 146 |
return label[label.index("(") + 1 : -1]
|
src/elbench/dashboard/leaderboard.py
CHANGED
|
@@ -61,6 +61,99 @@ FILE_TO_MODULE: dict[str, str] = {
|
|
| 61 |
|
| 62 |
_STORE_LOCK = threading.Lock()
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
def load_official_models() -> list[dict[str, Any]]:
|
| 66 |
if not OFFICIAL_DATA_PATH.exists():
|
|
@@ -69,25 +162,32 @@ def load_official_models() -> list[dict[str, Any]]:
|
|
| 69 |
return list(payload.get("official_models") or [])
|
| 70 |
|
| 71 |
|
| 72 |
-
def official_leaderboard_rows() -> list[list[Any]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
rows: list[list[Any]] = []
|
| 74 |
-
for item in
|
|
|
|
|
|
|
|
|
|
| 75 |
rows.append(
|
| 76 |
[
|
| 77 |
-
item["rank"],
|
| 78 |
item["display_name"],
|
| 79 |
-
|
| 80 |
-
round(float(
|
| 81 |
-
round(float(
|
| 82 |
-
round(float(
|
| 83 |
-
round(float(
|
| 84 |
-
item.get("items", 2939),
|
| 85 |
]
|
| 86 |
)
|
| 87 |
return rows
|
| 88 |
|
| 89 |
|
| 90 |
-
def official_per_file_rows(model_id_filter: str | None = None) -> list[list[Any]]:
|
| 91 |
rows: list[list[Any]] = []
|
| 92 |
models = load_official_models()
|
| 93 |
for item in models:
|
|
@@ -95,7 +195,10 @@ def official_per_file_rows(model_id_filter: str | None = None) -> list[list[Any]
|
|
| 95 |
if item["model_id"] != model_id_filter and item["display_name"] != model_id_filter:
|
| 96 |
continue
|
| 97 |
per_file = item.get("per_file") or {}
|
|
|
|
| 98 |
for fn, stats in per_file.items():
|
|
|
|
|
|
|
| 99 |
module_key = FILE_TO_MODULE.get(fn, "")
|
| 100 |
rows.append(
|
| 101 |
[
|
|
@@ -239,6 +342,7 @@ def compute_plan_fingerprint(
|
|
| 239 |
source_files: set[str] | frozenset[str],
|
| 240 |
dimensions: set[str] | frozenset[str],
|
| 241 |
max_samples: int | None,
|
|
|
|
| 242 |
) -> str:
|
| 243 |
payload = {
|
| 244 |
"target_model": openrouter_model.strip(),
|
|
@@ -251,6 +355,9 @@ def compute_plan_fingerprint(
|
|
| 251 |
"dimensions": sorted(dimensions),
|
| 252 |
"max_samples": max_samples,
|
| 253 |
}
|
|
|
|
|
|
|
|
|
|
| 254 |
encoded = json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")
|
| 255 |
return hashlib.sha256(encoded).hexdigest()[:16]
|
| 256 |
|
|
@@ -516,15 +623,34 @@ class LeaderboardStore:
|
|
| 516 |
tmp_path.replace(self.ledger_path)
|
| 517 |
|
| 518 |
|
| 519 |
-
def
|
| 520 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
rows: list[list[Any]] = []
|
| 522 |
for run in runs:
|
| 523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
judge_desc = str(run.get("judge_model") or "default")
|
| 525 |
if run.get("use_system_one_judge"):
|
| 526 |
judge_desc += " (System 1)"
|
| 527 |
-
scope_label = "Full benchmark" if run.get("is_full_benchmark") else "Part of the benchmark"
|
| 528 |
rows.append(
|
| 529 |
[
|
| 530 |
run.get("run_id"),
|
|
@@ -532,45 +658,55 @@ def custom_leaderboard_rows(runs: list[dict[str, Any]]) -> list[list[Any]]:
|
|
| 532 |
run.get("target_model"),
|
| 533 |
judge_desc,
|
| 534 |
run.get("status", "completed"),
|
| 535 |
-
|
| 536 |
run.get("total_judged", 0),
|
| 537 |
run.get("total_failures", 0),
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
]
|
| 544 |
)
|
| 545 |
return rows
|
| 546 |
|
| 547 |
|
| 548 |
-
def build_comparison_choices(
|
|
|
|
|
|
|
| 549 |
"""Return ``(primary_choices, baseline_choices)`` combining custom runs and official paper models."""
|
|
|
|
| 550 |
custom_items: list[tuple[str, str]] = []
|
| 551 |
for run in runs:
|
| 552 |
rid = str(run.get("run_id") or "")
|
| 553 |
model = str(run.get("target_model") or "unknown")
|
| 554 |
status = str(run.get("status") or "completed")
|
| 555 |
judged = int(run.get("total_judged") or 0)
|
| 556 |
-
overall =
|
| 557 |
-
|
|
|
|
|
|
|
|
|
|
| 558 |
label = f"Visitor run: {model} · {rid} ({judged} items, {status}, {score_str})"
|
| 559 |
custom_items.append((label, f"run:{rid}"))
|
| 560 |
|
| 561 |
official_items: list[tuple[str, str]] = []
|
| 562 |
for item in load_official_models():
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
)
|
| 567 |
official_items.append((label, f"official:{item['model_id']}"))
|
| 568 |
|
| 569 |
all_choices = custom_items + official_items
|
| 570 |
return all_choices, all_choices
|
| 571 |
|
| 572 |
|
| 573 |
-
def _resolve_comparison_entry(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
if not selector:
|
| 575 |
return None
|
| 576 |
if selector.startswith("run:"):
|
|
@@ -578,15 +714,12 @@ def _resolve_comparison_entry(selector: str, runs_by_id: dict[str, dict[str, Any
|
|
| 578 |
run = runs_by_id.get(rid)
|
| 579 |
if not run:
|
| 580 |
return None
|
| 581 |
-
norm = run.get("normalized_scores") or {}
|
| 582 |
return {
|
| 583 |
"label": f"{run.get('target_model')} ({rid})",
|
| 584 |
"kind": "Custom Run",
|
| 585 |
-
"
|
| 586 |
-
|
| 587 |
-
"safety": norm.get("safety_pct"),
|
| 588 |
-
"basic_education": norm.get("basic_education_pct"),
|
| 589 |
-
"high_level": norm.get("high_level_pct"),
|
| 590 |
"per_file": norm.get("per_file") or {},
|
| 591 |
}
|
| 592 |
if selector.startswith("official:"):
|
|
@@ -597,12 +730,8 @@ def _resolve_comparison_entry(selector: str, runs_by_id: dict[str, dict[str, Any
|
|
| 597 |
return {
|
| 598 |
"label": f"{off['display_name']} (Official)",
|
| 599 |
"kind": "Official Paper",
|
| 600 |
-
"
|
| 601 |
-
|
| 602 |
-
"safety": off.get("safety"),
|
| 603 |
-
"basic_education": off.get("basic_education"),
|
| 604 |
-
"high_level": off.get("high_level"),
|
| 605 |
-
"per_file": off.get("per_file") or {},
|
| 606 |
}
|
| 607 |
return None
|
| 608 |
|
|
@@ -612,12 +741,13 @@ def build_comparison_table(
|
|
| 612 |
baseline_selectors: list[str] | None,
|
| 613 |
granularity: str,
|
| 614 |
runs: list[dict[str, Any]],
|
|
|
|
| 615 |
) -> tuple[list[str], list[list[Any]], str]:
|
| 616 |
"""Build a side-by-side comparison table (headers, rows, note) between a primary run/model and baselines."""
|
| 617 |
runs_by_id = {str(r["run_id"]): r for r in runs if isinstance(r, dict) and r.get("run_id")}
|
| 618 |
official_by_id = {str(m["model_id"]): m for m in load_official_models()}
|
| 619 |
|
| 620 |
-
primary = _resolve_comparison_entry(primary_selector or "", runs_by_id, official_by_id)
|
| 621 |
if primary is None:
|
| 622 |
return (
|
| 623 |
["Metric", "Primary"],
|
|
@@ -629,7 +759,7 @@ def build_comparison_table(
|
|
| 629 |
for sel in baseline_selectors or []:
|
| 630 |
if sel == primary_selector:
|
| 631 |
continue
|
| 632 |
-
resolved = _resolve_comparison_entry(sel, runs_by_id, official_by_id)
|
| 633 |
if resolved is not None:
|
| 634 |
baselines.append(resolved)
|
| 635 |
|
|
@@ -675,12 +805,13 @@ def build_comparison_table(
|
|
| 675 |
"against a full 2,939-item run, switch to **By Source File / Task (Matched Subset)** for an "
|
| 676 |
"apples-to-apples comparison restricted to the exact files evaluated."
|
| 677 |
)
|
| 678 |
-
return headers, rows, note
|
| 679 |
|
| 680 |
# By Source File / Task (Matched Subset)
|
| 681 |
-
|
|
|
|
| 682 |
if not active_files:
|
| 683 |
-
active_files = list(
|
| 684 |
|
| 685 |
total_primary_items = 0
|
| 686 |
weighted_primary_sum = 0.0
|
|
@@ -737,7 +868,17 @@ def build_comparison_table(
|
|
| 737 |
f"`{primary['label']}`. The top row weights each baseline's score by the primary run's "
|
| 738 |
"evaluated item counts on those exact files."
|
| 739 |
)
|
| 740 |
-
return headers, rows, note
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
|
| 742 |
|
| 743 |
PUBLISHED_SOURCE = "Published result"
|
|
@@ -786,8 +927,14 @@ def full_file_counts() -> dict[str, int]:
|
|
| 786 |
return {name: int(stats.get("count") or 0) for name, stats in (models[0].get("per_file") or {}).items()}
|
| 787 |
|
| 788 |
|
| 789 |
-
def run_covers_metric(
|
| 790 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
|
| 792 |
A run that skipped items (a sample cap, a dimension filter, failures, or
|
| 793 |
unselected files) has a score over a different item set than the published
|
|
@@ -796,7 +943,10 @@ def run_covers_metric(run: dict[str, Any], score_metric: str, counts: dict[str,
|
|
| 796 |
if score_metric.startswith("file:"):
|
| 797 |
files: tuple[str, ...] = (score_metric.split(":", 1)[1],)
|
| 798 |
else:
|
| 799 |
-
|
|
|
|
|
|
|
|
|
|
| 800 |
full = counts if counts is not None else full_file_counts()
|
| 801 |
per_file = (run.get("normalized_scores") or {}).get("per_file") or {}
|
| 802 |
for name in files:
|
|
@@ -880,6 +1030,8 @@ def build_pareto_frontier(
|
|
| 880 |
cost_metric: str = "blended_per_1m",
|
| 881 |
include_custom_runs: bool = True,
|
| 882 |
keep: Callable[[dict[str, Any]], bool] | None = None,
|
|
|
|
|
|
|
| 883 |
) -> tuple[Any, list[list[Any]], str]:
|
| 884 |
"""Compute the cost-vs-score Pareto frontier and return ``(plotly_figure, table_rows, summary_md)``."""
|
| 885 |
from elbench.dashboard.protocol import catalog_slug
|
|
@@ -897,10 +1049,11 @@ def build_pareto_frontier(
|
|
| 897 |
return None, None
|
| 898 |
|
| 899 |
candidates: list[dict[str, Any]] = []
|
| 900 |
-
|
|
|
|
| 901 |
slug = off.get("openrouter_slug")
|
| 902 |
p_in, p_out = _lookup_price(slug, off.get("prompt_usd_per_token"), off.get("completion_usd_per_token"))
|
| 903 |
-
score_val = _extract_pareto_score(off, score_metric)
|
| 904 |
cost_val = _compute_cost_metric_value(p_in, p_out, cost_metric)
|
| 905 |
candidates.append(
|
| 906 |
{
|
|
@@ -911,7 +1064,8 @@ def build_pareto_frontier(
|
|
| 911 |
"completion_per_1m": round(p_out * 1_000_000.0, 3) if p_out is not None else None,
|
| 912 |
"cost": cost_val,
|
| 913 |
"score": round(score_val, 2) if score_val is not None else None,
|
| 914 |
-
|
|
|
|
| 915 |
}
|
| 916 |
)
|
| 917 |
|
|
@@ -919,22 +1073,16 @@ def build_pareto_frontier(
|
|
| 919 |
if include_custom_runs:
|
| 920 |
counts = full_file_counts()
|
| 921 |
for run in runs:
|
| 922 |
-
if not isinstance(run, dict):
|
| 923 |
continue
|
| 924 |
-
if not run_covers_metric(run, score_metric, counts):
|
| 925 |
partial_runs += 1
|
| 926 |
continue
|
| 927 |
target_slug = str(run.get("target_model") or "").strip()
|
| 928 |
rid = str(run.get("run_id") or "")
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
"general": norm.get("general_pct"),
|
| 933 |
-
"safety": norm.get("safety_pct"),
|
| 934 |
-
"basic_education": norm.get("basic_education_pct"),
|
| 935 |
-
"high_level": norm.get("high_level_pct"),
|
| 936 |
-
"per_file": norm.get("per_file") or {},
|
| 937 |
-
}
|
| 938 |
score_val = _extract_pareto_score(mapped, score_metric)
|
| 939 |
if score_val is None:
|
| 940 |
continue
|
|
@@ -1028,11 +1176,18 @@ def build_pareto_frontier(
|
|
| 1028 |
|
| 1029 |
frontier_names = [f"**{item['name']}** ({item['score']:.1f} at ${item['cost']:.2f})" for item in frontier]
|
| 1030 |
unpriced_names = [item["name"] for item in unpriced]
|
| 1031 |
-
summary_lines = [
|
| 1032 |
-
"**
|
| 1033 |
-
|
| 1034 |
-
+ ". Each of these scores higher than every model that costs less.",
|
| 1035 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
tied = statistically_tied_with_leader(candidates)
|
| 1037 |
if tied:
|
| 1038 |
summary_lines.append(
|
|
|
|
| 61 |
|
| 62 |
_STORE_LOCK = threading.Lock()
|
| 63 |
|
| 64 |
+
# Files keyed to China's national rules rather than to education: the general "should refuse" set
|
| 65 |
+
# (212 of 250 items labelled under China's "socialist core values" content rules) and C-Eval (Chinese
|
| 66 |
+
# national exams, including Marxism and Mao Zedong Thought). The published results are per file, so
|
| 67 |
+
# whole files are the finest cut that still re-scores the paper models. See D4 / T15.
|
| 68 |
+
CHINA_SPECIFIC_FILES = ("安全拒答.jsonl", "ceval_sampled.jsonl")
|
| 69 |
+
SCOPE_CORE = "core"
|
| 70 |
+
SCOPE_PAPER = "paper"
|
| 71 |
+
MODULE_SCORE_KEYS = {
|
| 72 |
+
"overall": "overall_pct",
|
| 73 |
+
"general": "general_pct",
|
| 74 |
+
"safety": "safety_pct",
|
| 75 |
+
"basic_education": "basic_education_pct",
|
| 76 |
+
"high_level": "high_level_pct",
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# The OpenRouter model closest to the paper's own hosted Qwen judge. Visitor runs it grades are ranked
|
| 81 |
+
# with the published models; runs graded by any other judge get a board of their own.
|
| 82 |
+
PAPER_JUDGE_SLUG = "qwen/qwen3-235b-a22b-2507"
|
| 83 |
+
PAPER_JUDGE_LABEL = "Qwen3-235B"
|
| 84 |
+
SYSTEM_ONE_PREFIX = "decision:"
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def judge_board(run: dict[str, Any]) -> str | None:
|
| 88 |
+
"""Board a visitor run is ranked on: its judge, or None when no answer needed a judge.
|
| 89 |
+
|
| 90 |
+
Runs saved before ``judge_used`` was recorded count as judged.
|
| 91 |
+
"""
|
| 92 |
+
if run.get("judge_used") is False:
|
| 93 |
+
return None
|
| 94 |
+
judge = str(run.get("judge_model") or "").strip()
|
| 95 |
+
if not judge or judge == "default":
|
| 96 |
+
return PAPER_JUDGE_SLUG
|
| 97 |
+
return f"{SYSTEM_ONE_PREFIX}{judge}" if run.get("use_system_one_judge") else judge
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def on_judge_board(run: dict[str, Any], board: str) -> bool:
|
| 101 |
+
"""A run without judged answers scores the same under every judge, so it belongs on every board."""
|
| 102 |
+
key = judge_board(run)
|
| 103 |
+
return key is None or key == board
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def judge_board_label(board: str) -> str:
|
| 107 |
+
if board == PAPER_JUDGE_SLUG:
|
| 108 |
+
return f"{PAPER_JUDGE_LABEL} (default, ranked with the published results)"
|
| 109 |
+
if board.startswith(SYSTEM_ONE_PREFIX):
|
| 110 |
+
return f"{board[len(SYSTEM_ONE_PREFIX):]} (decision model)"
|
| 111 |
+
return board
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def judge_board_choices(runs: list[dict[str, Any]]) -> list[tuple[str, str]]:
|
| 115 |
+
"""The default board first, then every other judge a visitor run used, with its run count."""
|
| 116 |
+
counts: dict[str, int] = {}
|
| 117 |
+
for run in runs:
|
| 118 |
+
key = judge_board(run) if isinstance(run, dict) else None
|
| 119 |
+
if key is not None and key != PAPER_JUDGE_SLUG:
|
| 120 |
+
counts[key] = counts.get(key, 0) + 1
|
| 121 |
+
choices = [(judge_board_label(PAPER_JUDGE_SLUG), PAPER_JUDGE_SLUG)]
|
| 122 |
+
for key in sorted(counts):
|
| 123 |
+
choices.append((f"{judge_board_label(key)} · {counts[key]} run{'s' if counts[key] != 1 else ''}", key))
|
| 124 |
+
return choices
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def scope_files(scope: str) -> tuple[str, ...]:
|
| 128 |
+
"""Files scored in ``scope``: every file for the paper's scope, all but the China-specific ones for core."""
|
| 129 |
+
if scope == SCOPE_PAPER:
|
| 130 |
+
return tuple(FILE_TO_MODULE)
|
| 131 |
+
return tuple(fn for fn in FILE_TO_MODULE if fn not in CHINA_SPECIFIC_FILES)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def scores_in_scope(per_file: dict[str, dict[str, Any]], scope: str) -> dict[str, Any]:
|
| 135 |
+
"""Module and overall scores over the files in ``scope``, by the paper's rules."""
|
| 136 |
+
keep = set(scope_files(scope))
|
| 137 |
+
return module_scores_from_per_file({fn: stats for fn, stats in per_file.items() if fn in keep})
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def official_scores(item: dict[str, Any], scope: str) -> dict[str, Any]:
|
| 141 |
+
"""A published model's scores keyed like ``MODULE_SCORE_KEYS`` (plus ``per_file``) in ``scope``.
|
| 142 |
+
|
| 143 |
+
The paper's scope uses the published numbers as printed; core recomputes them from per-file data.
|
| 144 |
+
"""
|
| 145 |
+
per_file = item.get("per_file") or {}
|
| 146 |
+
if scope == SCOPE_PAPER:
|
| 147 |
+
return {**{key: item.get(key) for key in MODULE_SCORE_KEYS}, "per_file": per_file}
|
| 148 |
+
norm = scores_in_scope(per_file, scope)
|
| 149 |
+
# per_file stays whole: a single-task view of a China-specific file is an explicit choice.
|
| 150 |
+
return {**{key: norm.get(field) for key, field in MODULE_SCORE_KEYS.items()}, "per_file": per_file}
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def scope_item_count(scope: str) -> int:
|
| 154 |
+
counts = full_file_counts()
|
| 155 |
+
return sum(counts.get(fn, 0) for fn in scope_files(scope))
|
| 156 |
+
|
| 157 |
|
| 158 |
def load_official_models() -> list[dict[str, Any]]:
|
| 159 |
if not OFFICIAL_DATA_PATH.exists():
|
|
|
|
| 162 |
return list(payload.get("official_models") or [])
|
| 163 |
|
| 164 |
|
| 165 |
+
def official_leaderboard_rows(scope: str = SCOPE_PAPER) -> list[list[Any]]:
|
| 166 |
+
"""Published models ranked in ``scope``. Only the paper's scope has a published 95% range."""
|
| 167 |
+
scored = [(item, official_scores(item, scope)) for item in load_official_models()]
|
| 168 |
+
scored.sort(key=lambda pair: -float(pair[1]["overall"] or 0.0))
|
| 169 |
+
items = scope_item_count(scope)
|
| 170 |
rows: list[list[Any]] = []
|
| 171 |
+
for rank, (item, scores) in enumerate(scored, start=1):
|
| 172 |
+
overall = f"{scores['overall']:.1f}"
|
| 173 |
+
if scope == SCOPE_PAPER:
|
| 174 |
+
overall += f" {item['ci_95']}"
|
| 175 |
rows.append(
|
| 176 |
[
|
| 177 |
+
rank if scope != SCOPE_PAPER else item["rank"],
|
| 178 |
item["display_name"],
|
| 179 |
+
overall,
|
| 180 |
+
round(float(scores["general"]), 1),
|
| 181 |
+
round(float(scores["safety"]), 1),
|
| 182 |
+
round(float(scores["basic_education"]), 1),
|
| 183 |
+
round(float(scores["high_level"]), 1),
|
| 184 |
+
item.get("items", 2939) if scope == SCOPE_PAPER else items,
|
| 185 |
]
|
| 186 |
)
|
| 187 |
return rows
|
| 188 |
|
| 189 |
|
| 190 |
+
def official_per_file_rows(model_id_filter: str | None = None, scope: str = SCOPE_PAPER) -> list[list[Any]]:
|
| 191 |
rows: list[list[Any]] = []
|
| 192 |
models = load_official_models()
|
| 193 |
for item in models:
|
|
|
|
| 195 |
if item["model_id"] != model_id_filter and item["display_name"] != model_id_filter:
|
| 196 |
continue
|
| 197 |
per_file = item.get("per_file") or {}
|
| 198 |
+
in_scope = set(scope_files(scope))
|
| 199 |
for fn, stats in per_file.items():
|
| 200 |
+
if fn not in in_scope:
|
| 201 |
+
continue
|
| 202 |
module_key = FILE_TO_MODULE.get(fn, "")
|
| 203 |
rows.append(
|
| 204 |
[
|
|
|
|
| 342 |
source_files: set[str] | frozenset[str],
|
| 343 |
dimensions: set[str] | frozenset[str],
|
| 344 |
max_samples: int | None,
|
| 345 |
+
max_samples_per_file: int | None = None,
|
| 346 |
) -> str:
|
| 347 |
payload = {
|
| 348 |
"target_model": openrouter_model.strip(),
|
|
|
|
| 355 |
"dimensions": sorted(dimensions),
|
| 356 |
"max_samples": max_samples,
|
| 357 |
}
|
| 358 |
+
if max_samples_per_file is not None:
|
| 359 |
+
# Only when set, so fingerprints of earlier runs (and their resume) are unchanged.
|
| 360 |
+
payload["max_samples_per_file"] = max_samples_per_file
|
| 361 |
encoded = json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")
|
| 362 |
return hashlib.sha256(encoded).hexdigest()[:16]
|
| 363 |
|
|
|
|
| 623 |
tmp_path.replace(self.ledger_path)
|
| 624 |
|
| 625 |
|
| 626 |
+
def run_coverage_label(run: dict[str, Any], counts: dict[str, int] | None = None) -> str:
|
| 627 |
+
if run_covers_metric(run, "overall", counts, scope=SCOPE_PAPER):
|
| 628 |
+
return "Full benchmark"
|
| 629 |
+
if run_covers_metric(run, "overall", counts, scope=SCOPE_CORE):
|
| 630 |
+
return "Complete, without the China-specific sets"
|
| 631 |
+
return "Part of the benchmark"
|
| 632 |
+
|
| 633 |
+
|
| 634 |
+
def custom_leaderboard_rows(runs: list[dict[str, Any]], scope: str = SCOPE_PAPER) -> list[list[Any]]:
|
| 635 |
+
"""Format persisted visitor runs for the visitor-run table.
|
| 636 |
+
|
| 637 |
+
A score is shown only for areas the run covered in full in ``scope``, so a short run never shows a
|
| 638 |
+
headline number next to complete ones.
|
| 639 |
+
"""
|
| 640 |
+
counts = full_file_counts()
|
| 641 |
rows: list[list[Any]] = []
|
| 642 |
for run in runs:
|
| 643 |
+
per_file = (run.get("normalized_scores") or {}).get("per_file") or {}
|
| 644 |
+
norm = scores_in_scope(per_file, scope)
|
| 645 |
+
|
| 646 |
+
def _cell(metric: str) -> Any:
|
| 647 |
+
if not run_covers_metric(run, metric, counts, scope=scope):
|
| 648 |
+
return None
|
| 649 |
+
return norm.get(MODULE_SCORE_KEYS[metric])
|
| 650 |
+
|
| 651 |
judge_desc = str(run.get("judge_model") or "default")
|
| 652 |
if run.get("use_system_one_judge"):
|
| 653 |
judge_desc += " (System 1)"
|
|
|
|
| 654 |
rows.append(
|
| 655 |
[
|
| 656 |
run.get("run_id"),
|
|
|
|
| 658 |
run.get("target_model"),
|
| 659 |
judge_desc,
|
| 660 |
run.get("status", "completed"),
|
| 661 |
+
run_coverage_label(run, counts),
|
| 662 |
run.get("total_judged", 0),
|
| 663 |
run.get("total_failures", 0),
|
| 664 |
+
_cell("overall"),
|
| 665 |
+
_cell("general"),
|
| 666 |
+
_cell("safety"),
|
| 667 |
+
_cell("basic_education"),
|
| 668 |
+
_cell("high_level"),
|
| 669 |
]
|
| 670 |
)
|
| 671 |
return rows
|
| 672 |
|
| 673 |
|
| 674 |
+
def build_comparison_choices(
|
| 675 |
+
runs: list[dict[str, Any]], scope: str = SCOPE_PAPER
|
| 676 |
+
) -> tuple[list[tuple[str, str]], list[tuple[str, str]]]:
|
| 677 |
"""Return ``(primary_choices, baseline_choices)`` combining custom runs and official paper models."""
|
| 678 |
+
counts = full_file_counts()
|
| 679 |
custom_items: list[tuple[str, str]] = []
|
| 680 |
for run in runs:
|
| 681 |
rid = str(run.get("run_id") or "")
|
| 682 |
model = str(run.get("target_model") or "unknown")
|
| 683 |
status = str(run.get("status") or "completed")
|
| 684 |
judged = int(run.get("total_judged") or 0)
|
| 685 |
+
overall = None
|
| 686 |
+
if run_covers_metric(run, "overall", counts, scope=scope):
|
| 687 |
+
per_file = (run.get("normalized_scores") or {}).get("per_file") or {}
|
| 688 |
+
overall = scores_in_scope(per_file, scope).get("overall_pct")
|
| 689 |
+
score_str = f"{overall:.1f}" if isinstance(overall, (int, float)) else "partial"
|
| 690 |
label = f"Visitor run: {model} · {rid} ({judged} items, {status}, {score_str})"
|
| 691 |
custom_items.append((label, f"run:{rid}"))
|
| 692 |
|
| 693 |
official_items: list[tuple[str, str]] = []
|
| 694 |
for item in load_official_models():
|
| 695 |
+
overall = official_scores(item, scope)["overall"]
|
| 696 |
+
range_text = f" {item['ci_95']}" if scope == SCOPE_PAPER else ""
|
| 697 |
+
label = f"Published: {item['display_name']} (Overall {overall:.1f}{range_text})"
|
|
|
|
| 698 |
official_items.append((label, f"official:{item['model_id']}"))
|
| 699 |
|
| 700 |
all_choices = custom_items + official_items
|
| 701 |
return all_choices, all_choices
|
| 702 |
|
| 703 |
|
| 704 |
+
def _resolve_comparison_entry(
|
| 705 |
+
selector: str,
|
| 706 |
+
runs_by_id: dict[str, dict[str, Any]],
|
| 707 |
+
official_by_id: dict[str, dict[str, Any]],
|
| 708 |
+
scope: str = SCOPE_PAPER,
|
| 709 |
+
) -> dict[str, Any] | None:
|
| 710 |
if not selector:
|
| 711 |
return None
|
| 712 |
if selector.startswith("run:"):
|
|
|
|
| 714 |
run = runs_by_id.get(rid)
|
| 715 |
if not run:
|
| 716 |
return None
|
| 717 |
+
norm = scores_in_scope((run.get("normalized_scores") or {}).get("per_file") or {}, scope)
|
| 718 |
return {
|
| 719 |
"label": f"{run.get('target_model')} ({rid})",
|
| 720 |
"kind": "Custom Run",
|
| 721 |
+
"judge": judge_board(run),
|
| 722 |
+
**{key: norm.get(field) for key, field in MODULE_SCORE_KEYS.items()},
|
|
|
|
|
|
|
|
|
|
| 723 |
"per_file": norm.get("per_file") or {},
|
| 724 |
}
|
| 725 |
if selector.startswith("official:"):
|
|
|
|
| 730 |
return {
|
| 731 |
"label": f"{off['display_name']} (Official)",
|
| 732 |
"kind": "Official Paper",
|
| 733 |
+
"judge": PAPER_JUDGE_SLUG,
|
| 734 |
+
**official_scores(off, scope),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
}
|
| 736 |
return None
|
| 737 |
|
|
|
|
| 741 |
baseline_selectors: list[str] | None,
|
| 742 |
granularity: str,
|
| 743 |
runs: list[dict[str, Any]],
|
| 744 |
+
scope: str = SCOPE_PAPER,
|
| 745 |
) -> tuple[list[str], list[list[Any]], str]:
|
| 746 |
"""Build a side-by-side comparison table (headers, rows, note) between a primary run/model and baselines."""
|
| 747 |
runs_by_id = {str(r["run_id"]): r for r in runs if isinstance(r, dict) and r.get("run_id")}
|
| 748 |
official_by_id = {str(m["model_id"]): m for m in load_official_models()}
|
| 749 |
|
| 750 |
+
primary = _resolve_comparison_entry(primary_selector or "", runs_by_id, official_by_id, scope)
|
| 751 |
if primary is None:
|
| 752 |
return (
|
| 753 |
["Metric", "Primary"],
|
|
|
|
| 759 |
for sel in baseline_selectors or []:
|
| 760 |
if sel == primary_selector:
|
| 761 |
continue
|
| 762 |
+
resolved = _resolve_comparison_entry(sel, runs_by_id, official_by_id, scope)
|
| 763 |
if resolved is not None:
|
| 764 |
baselines.append(resolved)
|
| 765 |
|
|
|
|
| 805 |
"against a full 2,939-item run, switch to **By Source File / Task (Matched Subset)** for an "
|
| 806 |
"apples-to-apples comparison restricted to the exact files evaluated."
|
| 807 |
)
|
| 808 |
+
return headers, rows, note + _judge_note([primary, *baselines])
|
| 809 |
|
| 810 |
# By Source File / Task (Matched Subset)
|
| 811 |
+
in_scope = scope_files(scope)
|
| 812 |
+
active_files = [fn for fn in in_scope if fn in primary_files and int(primary_files[fn].get("count") or 0) > 0]
|
| 813 |
if not active_files:
|
| 814 |
+
active_files = list(in_scope)
|
| 815 |
|
| 816 |
total_primary_items = 0
|
| 817 |
weighted_primary_sum = 0.0
|
|
|
|
| 868 |
f"`{primary['label']}`. The top row weights each baseline's score by the primary run's "
|
| 869 |
"evaluated item counts on those exact files."
|
| 870 |
)
|
| 871 |
+
return headers, rows, note + _judge_note([primary, *baselines])
|
| 872 |
+
|
| 873 |
+
|
| 874 |
+
def _judge_note(entries: list[dict[str, Any]]) -> str:
|
| 875 |
+
judges = sorted({entry["judge"] for entry in entries if entry.get("judge")})
|
| 876 |
+
if len(judges) < 2:
|
| 877 |
+
return ""
|
| 878 |
+
return (
|
| 879 |
+
f" **These results were graded by different judges** ({', '.join(judge_board_label(j) for j in judges)}), "
|
| 880 |
+
"so scores on open-ended tasks are not directly comparable. Automatically checked tasks are."
|
| 881 |
+
)
|
| 882 |
|
| 883 |
|
| 884 |
PUBLISHED_SOURCE = "Published result"
|
|
|
|
| 927 |
return {name: int(stats.get("count") or 0) for name, stats in (models[0].get("per_file") or {}).items()}
|
| 928 |
|
| 929 |
|
| 930 |
+
def run_covers_metric(
|
| 931 |
+
run: dict[str, Any],
|
| 932 |
+
score_metric: str,
|
| 933 |
+
counts: dict[str, int] | None = None,
|
| 934 |
+
*,
|
| 935 |
+
scope: str = SCOPE_PAPER,
|
| 936 |
+
) -> bool:
|
| 937 |
+
"""True when the run scored every item behind ``score_metric`` in ``scope``.
|
| 938 |
|
| 939 |
A run that skipped items (a sample cap, a dimension filter, failures, or
|
| 940 |
unselected files) has a score over a different item set than the published
|
|
|
|
| 943 |
if score_metric.startswith("file:"):
|
| 944 |
files: tuple[str, ...] = (score_metric.split(":", 1)[1],)
|
| 945 |
else:
|
| 946 |
+
in_scope = set(scope_files(scope))
|
| 947 |
+
files = tuple(
|
| 948 |
+
fn for fn in METRIC_SCOPE_FILES.get(score_metric, tuple(FILE_TO_MODULE)) if fn in in_scope
|
| 949 |
+
)
|
| 950 |
full = counts if counts is not None else full_file_counts()
|
| 951 |
per_file = (run.get("normalized_scores") or {}).get("per_file") or {}
|
| 952 |
for name in files:
|
|
|
|
| 1030 |
cost_metric: str = "blended_per_1m",
|
| 1031 |
include_custom_runs: bool = True,
|
| 1032 |
keep: Callable[[dict[str, Any]], bool] | None = None,
|
| 1033 |
+
scope: str = SCOPE_PAPER,
|
| 1034 |
+
judge: str = PAPER_JUDGE_SLUG,
|
| 1035 |
) -> tuple[Any, list[list[Any]], str]:
|
| 1036 |
"""Compute the cost-vs-score Pareto frontier and return ``(plotly_figure, table_rows, summary_md)``."""
|
| 1037 |
from elbench.dashboard.protocol import catalog_slug
|
|
|
|
| 1049 |
return None, None
|
| 1050 |
|
| 1051 |
candidates: list[dict[str, Any]] = []
|
| 1052 |
+
# The published results were graded by the paper's judge, so they sit on that board only.
|
| 1053 |
+
for off in load_official_models() if judge == PAPER_JUDGE_SLUG else []:
|
| 1054 |
slug = off.get("openrouter_slug")
|
| 1055 |
p_in, p_out = _lookup_price(slug, off.get("prompt_usd_per_token"), off.get("completion_usd_per_token"))
|
| 1056 |
+
score_val = _extract_pareto_score(official_scores(off, scope), score_metric)
|
| 1057 |
cost_val = _compute_cost_metric_value(p_in, p_out, cost_metric)
|
| 1058 |
candidates.append(
|
| 1059 |
{
|
|
|
|
| 1064 |
"completion_per_1m": round(p_out * 1_000_000.0, 3) if p_out is not None else None,
|
| 1065 |
"cost": cost_val,
|
| 1066 |
"score": round(score_val, 2) if score_val is not None else None,
|
| 1067 |
+
# The bootstrap ranges were published for the paper's scope only.
|
| 1068 |
+
"ci": official_ci(off, score_metric) if scope == SCOPE_PAPER else None,
|
| 1069 |
}
|
| 1070 |
)
|
| 1071 |
|
|
|
|
| 1073 |
if include_custom_runs:
|
| 1074 |
counts = full_file_counts()
|
| 1075 |
for run in runs:
|
| 1076 |
+
if not isinstance(run, dict) or not on_judge_board(run, judge):
|
| 1077 |
continue
|
| 1078 |
+
if not run_covers_metric(run, score_metric, counts, scope=scope):
|
| 1079 |
partial_runs += 1
|
| 1080 |
continue
|
| 1081 |
target_slug = str(run.get("target_model") or "").strip()
|
| 1082 |
rid = str(run.get("run_id") or "")
|
| 1083 |
+
per_file = (run.get("normalized_scores") or {}).get("per_file") or {}
|
| 1084 |
+
norm = scores_in_scope(per_file, scope)
|
| 1085 |
+
mapped = {**{key: norm.get(field) for key, field in MODULE_SCORE_KEYS.items()}, "per_file": per_file}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1086 |
score_val = _extract_pareto_score(mapped, score_metric)
|
| 1087 |
if score_val is None:
|
| 1088 |
continue
|
|
|
|
| 1176 |
|
| 1177 |
frontier_names = [f"**{item['name']}** ({item['score']:.1f} at ${item['cost']:.2f})" for item in frontier]
|
| 1178 |
unpriced_names = [item["name"] for item in unpriced]
|
| 1179 |
+
summary_lines = [] if judge == PAPER_JUDGE_SLUG else [
|
| 1180 |
+
f"**Graded by {judge_board_label(judge)}.** Only visitor runs with this judge are shown. The published "
|
| 1181 |
+
"results were graded by the paper's judge, so they are not on this board."
|
|
|
|
| 1182 |
]
|
| 1183 |
+
if judge != PAPER_JUDGE_SLUG and not priced:
|
| 1184 |
+
summary_lines.append("No complete runs with this judge yet. A complete run graded by it starts this board.")
|
| 1185 |
+
else:
|
| 1186 |
+
summary_lines.append(
|
| 1187 |
+
"**Best value:** "
|
| 1188 |
+
+ (" → ".join(frontier_names) if frontier_names else "none with a price yet")
|
| 1189 |
+
+ ". Each of these scores higher than every model that costs less."
|
| 1190 |
+
)
|
| 1191 |
tied = statistically_tied_with_leader(candidates)
|
| 1192 |
if tied:
|
| 1193 |
summary_lines.append(
|
src/elbench/dashboard/plan.py
CHANGED
|
@@ -26,7 +26,7 @@ from elbench.schemas.evaluation import Sample
|
|
| 26 |
|
| 27 |
from elbench.dashboard.catalog import Catalog, CatalogFile
|
| 28 |
from elbench.dashboard.english import english_file, english_module, english_preflight_message
|
| 29 |
-
from elbench.dashboard.leaderboard import compute_plan_fingerprint
|
| 30 |
from elbench.dashboard.protocol import (
|
| 31 |
VISITOR_KEY_ENV,
|
| 32 |
Caveat,
|
|
@@ -35,16 +35,22 @@ from elbench.dashboard.protocol import (
|
|
| 35 |
build_multiturn_chat_model_config,
|
| 36 |
build_visitor_protocol,
|
| 37 |
)
|
|
|
|
| 38 |
from elbench.schemas.config import ModelConfig
|
| 39 |
from elbench.utils.secrets import get_api_key
|
| 40 |
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
@dataclass(frozen=True, slots=True)
|
| 43 |
class RunSelection:
|
| 44 |
modules: frozenset[str]
|
| 45 |
source_files: frozenset[str]
|
| 46 |
dimensions: frozenset[str]
|
| 47 |
max_samples: int | None
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
@dataclass(slots=True)
|
|
@@ -77,10 +83,14 @@ class Plan:
|
|
| 77 |
use_multiturn_chat_model: bool = False
|
| 78 |
config_hash: str = ""
|
| 79 |
is_full_benchmark: bool = False
|
|
|
|
|
|
|
| 80 |
judge_prices: tuple[float, float] | None = None
|
| 81 |
student_prices: tuple[float, float] | None = None
|
| 82 |
# (low, high) USD per component: "target", "judge". Missing means no estimate.
|
| 83 |
component_usd: dict[str, tuple[float, float]] = field(default_factory=dict)
|
|
|
|
|
|
|
| 84 |
|
| 85 |
def error_messages(self) -> list[str]:
|
| 86 |
return [caveat.message for caveat in self.caveats if caveat.level == "error"]
|
|
@@ -97,6 +107,7 @@ def build_plan(
|
|
| 97 |
prompt_usd_per_token: float | None,
|
| 98 |
completion_usd_per_token: float | None,
|
| 99 |
max_concurrency: int,
|
|
|
|
| 100 |
judge_openrouter_model: str | None = None,
|
| 101 |
use_system_one_judge: bool = False,
|
| 102 |
judge_prompt_usd_per_token: float | None = None,
|
|
@@ -105,6 +116,7 @@ def build_plan(
|
|
| 105 |
multiturn_openrouter_model: str | None = None,
|
| 106 |
multiturn_prompt_usd_per_token: float | None = None,
|
| 107 |
multiturn_completion_usd_per_token: float | None = None,
|
|
|
|
| 108 |
) -> Plan:
|
| 109 |
protocol = build_visitor_protocol(catalog.config, openrouter_model)
|
| 110 |
config = catalog.config.model_copy(deep=True)
|
|
@@ -114,6 +126,7 @@ def build_plan(
|
|
| 114 |
source_files=frozenset(source_files),
|
| 115 |
dimensions=frozenset(dimensions or ()),
|
| 116 |
max_samples=max_samples,
|
|
|
|
| 117 |
)
|
| 118 |
caveats = list(protocol.caveats)
|
| 119 |
|
|
@@ -189,6 +202,20 @@ def build_plan(
|
|
| 189 |
),
|
| 190 |
)
|
| 191 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
caveats.extend(_selection_caveats(catalog, selection, max_concurrency))
|
| 193 |
|
| 194 |
samples = iter_standard_samples(config, selection)
|
|
@@ -281,9 +308,10 @@ def build_plan(
|
|
| 281 |
and judge_prompt_usd_per_token is not None
|
| 282 |
and judge_prompt_usd_per_token >= 0
|
| 283 |
):
|
| 284 |
-
# Each judge call sends state + rubric (~300 tokens low, ~1024 tokens high)
|
|
|
|
| 285 |
judge_low = judge_calls * 300 * judge_prompt_usd_per_token
|
| 286 |
-
judge_out_cap = 0 if use_system_one_judge else
|
| 287 |
judge_comp_price = max(0.0, float(judge_completion_usd_per_token or 0.0))
|
| 288 |
judge_high = (judge_calls * 1024 * judge_prompt_usd_per_token) + (
|
| 289 |
judge_calls * judge_out_cap * judge_comp_price
|
|
@@ -309,6 +337,7 @@ def build_plan(
|
|
| 309 |
blocked = any(caveat.level == "error" for caveat in caveats)
|
| 310 |
preflight_ok = not any(caveat.code.startswith("preflight_") and caveat.level == "error" for caveat in caveats)
|
| 311 |
is_full = any(caveat.code == "full_item_set" for caveat in caveats)
|
|
|
|
| 312 |
cfg_hash = compute_plan_fingerprint(
|
| 313 |
openrouter_model=protocol.openrouter_model,
|
| 314 |
judge_openrouter_model=cleaned_judge,
|
|
@@ -319,6 +348,7 @@ def build_plan(
|
|
| 319 |
source_files=selection.source_files,
|
| 320 |
dimensions=selection.dimensions,
|
| 321 |
max_samples=selection.max_samples,
|
|
|
|
| 322 |
)
|
| 323 |
return Plan(
|
| 324 |
protocol=protocol,
|
|
@@ -349,6 +379,7 @@ def build_plan(
|
|
| 349 |
use_multiturn_chat_model=bool(use_multiturn_chat_model),
|
| 350 |
config_hash=cfg_hash,
|
| 351 |
is_full_benchmark=is_full,
|
|
|
|
| 352 |
judge_prices=_price_tuple(judge_prompt_usd_per_token, judge_completion_usd_per_token),
|
| 353 |
student_prices=(
|
| 354 |
_price_tuple(multiturn_prompt_usd_per_token, multiturn_completion_usd_per_token)
|
|
@@ -356,6 +387,36 @@ def build_plan(
|
|
| 356 |
else None
|
| 357 |
),
|
| 358 |
component_usd=component_usd,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
)
|
| 360 |
|
| 361 |
|
|
@@ -379,7 +440,7 @@ def iter_standard_samples(config: ProjectConfig, selection: RunSelection) -> lis
|
|
| 379 |
)
|
| 380 |
dimensions = set(selection.dimensions) or None
|
| 381 |
runner = BenchmarkRunner(config)
|
| 382 |
-
return list(runner._load_samples(resolved, dimensions, selection.max_samples))
|
| 383 |
|
| 384 |
|
| 385 |
def render_plan(plan: Plan) -> str:
|
|
@@ -492,6 +553,7 @@ def _selection_caveats(catalog: Catalog, selection: RunSelection, max_concurrenc
|
|
| 492 |
and full_files
|
| 493 |
and not selection.dimensions
|
| 494 |
and selection.max_samples is None
|
|
|
|
| 495 |
)
|
| 496 |
if not full_items:
|
| 497 |
reasons: list[str] = []
|
|
@@ -508,6 +570,8 @@ def _selection_caveats(catalog: Catalog, selection: RunSelection, max_concurrenc
|
|
| 508 |
f"max_samples={selection.max_samples} keeps a prefix in registry order, "
|
| 509 |
"the same order BenchmarkRunner uses, not a random or stratified sample"
|
| 510 |
)
|
|
|
|
|
|
|
| 511 |
caveats.append(
|
| 512 |
Caveat(
|
| 513 |
"warning",
|
|
@@ -673,6 +737,8 @@ def _basic_education_bounds(
|
|
| 673 |
)
|
| 674 |
)
|
| 675 |
take = declared if remaining is None else min(declared, remaining)
|
|
|
|
|
|
|
| 676 |
globals_section = template.get("globals") if isinstance(template, dict) else {}
|
| 677 |
raw_limit = globals_section.get("recursion_limit") if isinstance(globals_section, dict) else None
|
| 678 |
try:
|
|
|
|
| 26 |
|
| 27 |
from elbench.dashboard.catalog import Catalog, CatalogFile
|
| 28 |
from elbench.dashboard.english import english_file, english_module, english_preflight_message
|
| 29 |
+
from elbench.dashboard.leaderboard import CHINA_SPECIFIC_FILES, compute_plan_fingerprint
|
| 30 |
from elbench.dashboard.protocol import (
|
| 31 |
VISITOR_KEY_ENV,
|
| 32 |
Caveat,
|
|
|
|
| 35 |
build_multiturn_chat_model_config,
|
| 36 |
build_visitor_protocol,
|
| 37 |
)
|
| 38 |
+
from elbench.dashboard.routing import Route
|
| 39 |
from elbench.schemas.config import ModelConfig
|
| 40 |
from elbench.utils.secrets import get_api_key
|
| 41 |
|
| 42 |
|
| 43 |
+
# High-end assumption for one judge grade, thinking included. Not sent as a limit.
|
| 44 |
+
JUDGE_HIGH_OUTPUT_TOKENS = 8192
|
| 45 |
+
|
| 46 |
+
|
| 47 |
@dataclass(frozen=True, slots=True)
|
| 48 |
class RunSelection:
|
| 49 |
modules: frozenset[str]
|
| 50 |
source_files: frozenset[str]
|
| 51 |
dimensions: frozenset[str]
|
| 52 |
max_samples: int | None
|
| 53 |
+
max_samples_per_file: int | None = None
|
| 54 |
|
| 55 |
|
| 56 |
@dataclass(slots=True)
|
|
|
|
| 83 |
use_multiturn_chat_model: bool = False
|
| 84 |
config_hash: str = ""
|
| 85 |
is_full_benchmark: bool = False
|
| 86 |
+
# Every question outside the China-specific sets is selected (the default "core" scope).
|
| 87 |
+
covers_core: bool = False
|
| 88 |
judge_prices: tuple[float, float] | None = None
|
| 89 |
student_prices: tuple[float, float] | None = None
|
| 90 |
# (low, high) USD per component: "target", "judge". Missing means no estimate.
|
| 91 |
component_usd: dict[str, tuple[float, float]] = field(default_factory=dict)
|
| 92 |
+
# Host choice per role ("target", "judge", "student") that the request bodies carry.
|
| 93 |
+
routes: dict[str, Route] = field(default_factory=dict)
|
| 94 |
|
| 95 |
def error_messages(self) -> list[str]:
|
| 96 |
return [caveat.message for caveat in self.caveats if caveat.level == "error"]
|
|
|
|
| 107 |
prompt_usd_per_token: float | None,
|
| 108 |
completion_usd_per_token: float | None,
|
| 109 |
max_concurrency: int,
|
| 110 |
+
max_samples_per_file: int | None = None,
|
| 111 |
judge_openrouter_model: str | None = None,
|
| 112 |
use_system_one_judge: bool = False,
|
| 113 |
judge_prompt_usd_per_token: float | None = None,
|
|
|
|
| 116 |
multiturn_openrouter_model: str | None = None,
|
| 117 |
multiturn_prompt_usd_per_token: float | None = None,
|
| 118 |
multiturn_completion_usd_per_token: float | None = None,
|
| 119 |
+
routes: dict[str, Route] | None = None,
|
| 120 |
) -> Plan:
|
| 121 |
protocol = build_visitor_protocol(catalog.config, openrouter_model)
|
| 122 |
config = catalog.config.model_copy(deep=True)
|
|
|
|
| 126 |
source_files=frozenset(source_files),
|
| 127 |
dimensions=frozenset(dimensions or ()),
|
| 128 |
max_samples=max_samples,
|
| 129 |
+
max_samples_per_file=max_samples_per_file,
|
| 130 |
)
|
| 131 |
caveats = list(protocol.caveats)
|
| 132 |
|
|
|
|
| 202 |
),
|
| 203 |
)
|
| 204 |
)
|
| 205 |
+
applied_routes = _apply_routes(
|
| 206 |
+
routes or {},
|
| 207 |
+
target=protocol.model_config,
|
| 208 |
+
judge=None if use_system_one_judge else judge_model_config,
|
| 209 |
+
student=multiturn_model_config,
|
| 210 |
+
)
|
| 211 |
+
if applied_routes:
|
| 212 |
+
caveats.append(
|
| 213 |
+
Caveat(
|
| 214 |
+
level="info",
|
| 215 |
+
code="openrouter_host_choice",
|
| 216 |
+
message=" ".join(route.describe() for route in applied_routes.values()),
|
| 217 |
+
)
|
| 218 |
+
)
|
| 219 |
caveats.extend(_selection_caveats(catalog, selection, max_concurrency))
|
| 220 |
|
| 221 |
samples = iter_standard_samples(config, selection)
|
|
|
|
| 308 |
and judge_prompt_usd_per_token is not None
|
| 309 |
and judge_prompt_usd_per_token >= 0
|
| 310 |
):
|
| 311 |
+
# Each judge call sends state + rubric (~300 tokens low, ~1024 tokens high). The judge has no
|
| 312 |
+
# length limit, so the high end assumes JUDGE_HIGH_OUTPUT_TOKENS of output, thinking included.
|
| 313 |
judge_low = judge_calls * 300 * judge_prompt_usd_per_token
|
| 314 |
+
judge_out_cap = 0 if use_system_one_judge else JUDGE_HIGH_OUTPUT_TOKENS
|
| 315 |
judge_comp_price = max(0.0, float(judge_completion_usd_per_token or 0.0))
|
| 316 |
judge_high = (judge_calls * 1024 * judge_prompt_usd_per_token) + (
|
| 317 |
judge_calls * judge_out_cap * judge_comp_price
|
|
|
|
| 337 |
blocked = any(caveat.level == "error" for caveat in caveats)
|
| 338 |
preflight_ok = not any(caveat.code.startswith("preflight_") and caveat.level == "error" for caveat in caveats)
|
| 339 |
is_full = any(caveat.code == "full_item_set" for caveat in caveats)
|
| 340 |
+
covers_core = _covers_core(catalog, selection)
|
| 341 |
cfg_hash = compute_plan_fingerprint(
|
| 342 |
openrouter_model=protocol.openrouter_model,
|
| 343 |
judge_openrouter_model=cleaned_judge,
|
|
|
|
| 348 |
source_files=selection.source_files,
|
| 349 |
dimensions=selection.dimensions,
|
| 350 |
max_samples=selection.max_samples,
|
| 351 |
+
max_samples_per_file=selection.max_samples_per_file,
|
| 352 |
)
|
| 353 |
return Plan(
|
| 354 |
protocol=protocol,
|
|
|
|
| 379 |
use_multiturn_chat_model=bool(use_multiturn_chat_model),
|
| 380 |
config_hash=cfg_hash,
|
| 381 |
is_full_benchmark=is_full,
|
| 382 |
+
covers_core=covers_core,
|
| 383 |
judge_prices=_price_tuple(judge_prompt_usd_per_token, judge_completion_usd_per_token),
|
| 384 |
student_prices=(
|
| 385 |
_price_tuple(multiturn_prompt_usd_per_token, multiturn_completion_usd_per_token)
|
|
|
|
| 387 |
else None
|
| 388 |
),
|
| 389 |
component_usd=component_usd,
|
| 390 |
+
routes=applied_routes,
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
def _apply_routes(routes: dict[str, Route], **configs: ModelConfig | None) -> dict[str, Route]:
|
| 395 |
+
"""Put each role's OpenRouter ``provider`` object into its request body. Decision models are not routed."""
|
| 396 |
+
applied: dict[str, Route] = {}
|
| 397 |
+
for role, model_config in configs.items():
|
| 398 |
+
route = routes.get(role)
|
| 399 |
+
if model_config is None or route is None or not route.provider:
|
| 400 |
+
continue
|
| 401 |
+
model_config.provider_kwargs = {**model_config.provider_kwargs, "provider": dict(route.provider)}
|
| 402 |
+
applied[role] = route
|
| 403 |
+
return applied
|
| 404 |
+
|
| 405 |
+
|
| 406 |
+
def _covers_core(catalog: Catalog, selection: RunSelection) -> bool:
|
| 407 |
+
"""True when every question outside the China-specific sets is selected, uncapped and unfiltered."""
|
| 408 |
+
active = [
|
| 409 |
+
name
|
| 410 |
+
for name in catalog.config.app.benchmark_modules.active
|
| 411 |
+
if name in catalog.config.modules and catalog.config.modules[name].enabled
|
| 412 |
+
]
|
| 413 |
+
core = {entry.canonical_name for entry in catalog.files} - set(CHINA_SPECIFIC_FILES)
|
| 414 |
+
return (
|
| 415 |
+
set(selection.modules) == set(active)
|
| 416 |
+
and core <= set(selection.source_files)
|
| 417 |
+
and not selection.dimensions
|
| 418 |
+
and selection.max_samples is None
|
| 419 |
+
and selection.max_samples_per_file is None
|
| 420 |
)
|
| 421 |
|
| 422 |
|
|
|
|
| 440 |
)
|
| 441 |
dimensions = set(selection.dimensions) or None
|
| 442 |
runner = BenchmarkRunner(config)
|
| 443 |
+
return list(runner._load_samples(resolved, dimensions, selection.max_samples, selection.max_samples_per_file))
|
| 444 |
|
| 445 |
|
| 446 |
def render_plan(plan: Plan) -> str:
|
|
|
|
| 553 |
and full_files
|
| 554 |
and not selection.dimensions
|
| 555 |
and selection.max_samples is None
|
| 556 |
+
and selection.max_samples_per_file is None
|
| 557 |
)
|
| 558 |
if not full_items:
|
| 559 |
reasons: list[str] = []
|
|
|
|
| 570 |
f"max_samples={selection.max_samples} keeps a prefix in registry order, "
|
| 571 |
"the same order BenchmarkRunner uses, not a random or stratified sample"
|
| 572 |
)
|
| 573 |
+
if selection.max_samples_per_file is not None:
|
| 574 |
+
reasons.append(f"max_samples_per_file={selection.max_samples_per_file} keeps a prefix of each file")
|
| 575 |
caveats.append(
|
| 576 |
Caveat(
|
| 577 |
"warning",
|
|
|
|
| 737 |
)
|
| 738 |
)
|
| 739 |
take = declared if remaining is None else min(declared, remaining)
|
| 740 |
+
if selection.max_samples_per_file is not None:
|
| 741 |
+
take = min(take, selection.max_samples_per_file)
|
| 742 |
globals_section = template.get("globals") if isinstance(template, dict) else {}
|
| 743 |
raw_limit = globals_section.get("recursion_limit") if isinstance(globals_section, dict) else None
|
| 744 |
try:
|
src/elbench/dashboard/protocol.py
CHANGED
|
@@ -249,12 +249,12 @@ def build_visitor_protocol(config: ProjectConfig, openrouter_model: str) -> Visi
|
|
| 249 |
level="warning",
|
| 250 |
code="openrouter_provider_routing",
|
| 251 |
message=(
|
| 252 |
-
"OpenRouter
|
| 253 |
-
"
|
| 254 |
-
"
|
| 255 |
-
"
|
| 256 |
-
"
|
| 257 |
-
"
|
| 258 |
"POST /api/v1/chat/completions authenticates with a Bearer token. "
|
| 259 |
"Reported token counts use the model's native tokenizer."
|
| 260 |
),
|
|
@@ -329,7 +329,8 @@ def build_judge_model_config(
|
|
| 329 |
update={
|
| 330 |
"model_id": f"judge_{safe_model_id(model_name)}",
|
| 331 |
"temperature": 0.0,
|
| 332 |
-
|
|
|
|
| 333 |
},
|
| 334 |
deep=True,
|
| 335 |
)
|
|
@@ -348,7 +349,8 @@ def build_multiturn_chat_model_config(
|
|
| 348 |
update={
|
| 349 |
"model_id": f"multiturn_{safe_model_id(model_name)}",
|
| 350 |
"temperature": 0.0,
|
| 351 |
-
|
|
|
|
| 352 |
},
|
| 353 |
deep=True,
|
| 354 |
)
|
|
|
|
| 249 |
level="warning",
|
| 250 |
code="openrouter_provider_routing",
|
| 251 |
message=(
|
| 252 |
+
"OpenRouter serves this model slug from several hosts. The run limits them to the "
|
| 253 |
+
"highest precision any host reports and tries them cheapest first (see the host note). "
|
| 254 |
+
"allow_fallbacks defaults to true, so a host error is retried on another host at the "
|
| 255 |
+
"same precision. require_parameters defaults to false, so a host that does not implement "
|
| 256 |
+
"temperature or max_tokens still accepts the request and ignores those fields. "
|
| 257 |
+
"The model field is the slug above. "
|
| 258 |
"POST /api/v1/chat/completions authenticates with a Bearer token. "
|
| 259 |
"Reported token counts use the model's native tokenizer."
|
| 260 |
),
|
|
|
|
| 329 |
update={
|
| 330 |
"model_id": f"judge_{safe_model_id(model_name)}",
|
| 331 |
"temperature": 0.0,
|
| 332 |
+
# No length limit: a reasoning judge thinks before it grades, and a cap cuts off the grade.
|
| 333 |
+
"max_tokens": None,
|
| 334 |
},
|
| 335 |
deep=True,
|
| 336 |
)
|
|
|
|
| 349 |
update={
|
| 350 |
"model_id": f"multiturn_{safe_model_id(model_name)}",
|
| 351 |
"temperature": 0.0,
|
| 352 |
+
# No length limit, for the same reason as the judge: a reasoning student needs room to think.
|
| 353 |
+
"max_tokens": None,
|
| 354 |
},
|
| 355 |
deep=True,
|
| 356 |
)
|
src/elbench/dashboard/routing.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pick OpenRouter hosts for a model: the highest precision on offer, then the lowest price.
|
| 2 |
+
|
| 3 |
+
OpenRouter lists every host of a model with its quantization and price at
|
| 4 |
+
``GET /api/v1/models/{slug}/endpoints`` (no key needed). The request body then
|
| 5 |
+
carries a ``provider`` object that limits routing to that precision and sorts
|
| 6 |
+
the remaining hosts by price, so fallbacks stay at the same precision.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import threading
|
| 12 |
+
import time
|
| 13 |
+
from dataclasses import dataclass, field
|
| 14 |
+
from typing import Any
|
| 15 |
+
|
| 16 |
+
import httpx
|
| 17 |
+
|
| 18 |
+
ENDPOINTS_URL = "https://openrouter.ai/api/v1/models/{slug}/endpoints"
|
| 19 |
+
CACHE_SECONDS = 900.0
|
| 20 |
+
|
| 21 |
+
# Bits per weight, floating point ahead of integer at the same width. "unknown" is not ranked:
|
| 22 |
+
# when any host reports a precision, hosts that report none are left out.
|
| 23 |
+
PRECISION_RANK: dict[str, float] = {
|
| 24 |
+
"fp32": 32,
|
| 25 |
+
"bf16": 16,
|
| 26 |
+
"fp16": 16,
|
| 27 |
+
"fp8": 8,
|
| 28 |
+
"int8": 7.5,
|
| 29 |
+
"fp6": 6,
|
| 30 |
+
"fp4": 4,
|
| 31 |
+
"nvfp4": 4,
|
| 32 |
+
"mxfp4": 4,
|
| 33 |
+
"int4": 3.5,
|
| 34 |
+
}
|
| 35 |
+
# Hosts OpenRouter only uses when the request asks for that tier.
|
| 36 |
+
OPT_IN_SERVICE_TIERS = {"priority"}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclass(slots=True)
|
| 40 |
+
class Host:
|
| 41 |
+
name: str
|
| 42 |
+
quantization: str
|
| 43 |
+
prompt_usd_per_token: float | None
|
| 44 |
+
completion_usd_per_token: float | None
|
| 45 |
+
service_tier: str | None = None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass(slots=True)
|
| 49 |
+
class Route:
|
| 50 |
+
slug: str
|
| 51 |
+
# The OpenRouter ``provider`` request field; empty when the hosts could not be read.
|
| 52 |
+
provider: dict[str, Any] = field(default_factory=dict)
|
| 53 |
+
precisions: list[str] = field(default_factory=list)
|
| 54 |
+
hosts: list[Host] = field(default_factory=list)
|
| 55 |
+
error: str | None = None
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def cheapest(self) -> Host | None:
|
| 59 |
+
priced = [h for h in self.hosts if h.prompt_usd_per_token is not None and h.completion_usd_per_token is not None]
|
| 60 |
+
return min(priced, key=lambda h: (h.prompt_usd_per_token, h.completion_usd_per_token), default=None)
|
| 61 |
+
|
| 62 |
+
def describe(self) -> str:
|
| 63 |
+
"""One plain sentence for the estimate."""
|
| 64 |
+
if self.error:
|
| 65 |
+
return f"`{self.slug}`: host list unavailable ({self.error}), so the cheapest host is used at any precision."
|
| 66 |
+
host = self.cheapest
|
| 67 |
+
where = f", starting with {host.name}" if host else ""
|
| 68 |
+
count = len(self.hosts)
|
| 69 |
+
hosts = f"{count} host{'s' if count != 1 else ''}"
|
| 70 |
+
if self.precisions:
|
| 71 |
+
return f"`{self.slug}`: {' / '.join(self.precisions)} only ({hosts}), cheapest first{where}."
|
| 72 |
+
return f"`{self.slug}`: no host reports its precision, so the cheapest of {hosts} is used{where}."
|
| 73 |
+
|
| 74 |
+
def record(self) -> dict[str, Any]:
|
| 75 |
+
"""What the run record keeps, so a result says which precision produced it."""
|
| 76 |
+
host = self.cheapest
|
| 77 |
+
return {
|
| 78 |
+
"provider": dict(self.provider),
|
| 79 |
+
"precisions": list(self.precisions),
|
| 80 |
+
"cheapest_host": host.name if host else None,
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _price(value: Any) -> float | None:
|
| 85 |
+
try:
|
| 86 |
+
price = float(value)
|
| 87 |
+
except (TypeError, ValueError):
|
| 88 |
+
return None
|
| 89 |
+
return price if price >= 0 else None
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def route_from_endpoints(slug: str, endpoints: list[dict[str, Any]]) -> Route:
|
| 93 |
+
hosts = [
|
| 94 |
+
Host(
|
| 95 |
+
name=str(e.get("provider_name") or e.get("tag") or "?"),
|
| 96 |
+
quantization=str(e.get("quantization") or "unknown").lower(),
|
| 97 |
+
prompt_usd_per_token=_price((e.get("pricing") or {}).get("prompt")),
|
| 98 |
+
completion_usd_per_token=_price((e.get("pricing") or {}).get("completion")),
|
| 99 |
+
service_tier=e.get("service_tier"),
|
| 100 |
+
)
|
| 101 |
+
for e in endpoints
|
| 102 |
+
if isinstance(e, dict)
|
| 103 |
+
]
|
| 104 |
+
hosts = [h for h in hosts if h.service_tier not in OPT_IN_SERVICE_TIERS]
|
| 105 |
+
# ":nitro" already asks OpenRouter to sort by speed; every other id is sorted by price.
|
| 106 |
+
provider: dict[str, Any] = {} if slug.endswith(":nitro") else {"sort": "price"}
|
| 107 |
+
ranked = [h for h in hosts if h.quantization in PRECISION_RANK]
|
| 108 |
+
if not ranked:
|
| 109 |
+
return Route(slug=slug, provider=provider, hosts=hosts)
|
| 110 |
+
best = max(PRECISION_RANK[h.quantization] for h in ranked)
|
| 111 |
+
precisions = sorted({h.quantization for h in ranked if PRECISION_RANK[h.quantization] == best})
|
| 112 |
+
return Route(
|
| 113 |
+
slug=slug,
|
| 114 |
+
provider={**provider, "quantizations": precisions},
|
| 115 |
+
precisions=precisions,
|
| 116 |
+
hosts=[h for h in ranked if h.quantization in precisions],
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
_cache: dict[str, tuple[float, Route]] = {}
|
| 121 |
+
_lock = threading.Lock()
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def resolve_route(slug: str, *, client: httpx.Client | None = None) -> Route:
|
| 125 |
+
"""Route for ``slug``, cached for ``CACHE_SECONDS``. A failed lookup still sorts by price."""
|
| 126 |
+
slug = slug.strip()
|
| 127 |
+
now = time.monotonic()
|
| 128 |
+
with _lock:
|
| 129 |
+
cached = _cache.get(slug)
|
| 130 |
+
if cached and now - cached[0] < CACHE_SECONDS:
|
| 131 |
+
return cached[1]
|
| 132 |
+
path_slug = slug[: -len(":nitro")] if slug.endswith(":nitro") else slug
|
| 133 |
+
path_slug = path_slug[: -len(":floor")] if path_slug.endswith(":floor") else path_slug
|
| 134 |
+
try:
|
| 135 |
+
if client is None:
|
| 136 |
+
with httpx.Client(timeout=10.0) as own:
|
| 137 |
+
response = own.get(ENDPOINTS_URL.format(slug=path_slug))
|
| 138 |
+
else:
|
| 139 |
+
response = client.get(ENDPOINTS_URL.format(slug=path_slug))
|
| 140 |
+
response.raise_for_status()
|
| 141 |
+
endpoints = ((response.json() or {}).get("data") or {}).get("endpoints") or []
|
| 142 |
+
route = route_from_endpoints(slug, endpoints)
|
| 143 |
+
except (httpx.HTTPError, ValueError) as exc:
|
| 144 |
+
reason = f"HTTP {exc.response.status_code}" if isinstance(exc, httpx.HTTPStatusError) else type(exc).__name__
|
| 145 |
+
return Route(slug=slug, provider={} if slug.endswith(":nitro") else {"sort": "price"}, error=reason)
|
| 146 |
+
with _lock:
|
| 147 |
+
_cache[slug] = (now, route)
|
| 148 |
+
return route
|
src/elbench/dashboard/run.py
CHANGED
|
@@ -156,6 +156,7 @@ def execute_run(
|
|
| 156 |
source_files=set(plan.selection.source_files),
|
| 157 |
dimensions=set(plan.selection.dimensions) or None,
|
| 158 |
max_samples=plan.selection.max_samples,
|
|
|
|
| 159 |
max_concurrency=max_concurrency,
|
| 160 |
resume=bool(resume_previous),
|
| 161 |
judge_enabled=True,
|
|
@@ -225,8 +226,13 @@ def execute_run(
|
|
| 225 |
"modules": [english_module(m) for m in sorted(plan.selection.modules)],
|
| 226 |
"source_files": sorted(plan.selection.source_files),
|
| 227 |
"max_samples": plan.selection.max_samples,
|
|
|
|
| 228 |
"dimensions": sorted(plan.selection.dimensions),
|
| 229 |
"is_full_benchmark": bool(plan.is_full_benchmark),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
"status": run_status,
|
| 231 |
"resumed": resumed_from_existing,
|
| 232 |
"total_judged": total_judged,
|
|
|
|
| 156 |
source_files=set(plan.selection.source_files),
|
| 157 |
dimensions=set(plan.selection.dimensions) or None,
|
| 158 |
max_samples=plan.selection.max_samples,
|
| 159 |
+
max_samples_per_file=plan.selection.max_samples_per_file,
|
| 160 |
max_concurrency=max_concurrency,
|
| 161 |
resume=bool(resume_previous),
|
| 162 |
judge_enabled=True,
|
|
|
|
| 226 |
"modules": [english_module(m) for m in sorted(plan.selection.modules)],
|
| 227 |
"source_files": sorted(plan.selection.source_files),
|
| 228 |
"max_samples": plan.selection.max_samples,
|
| 229 |
+
"max_samples_per_file": plan.selection.max_samples_per_file,
|
| 230 |
"dimensions": sorted(plan.selection.dimensions),
|
| 231 |
"is_full_benchmark": bool(plan.is_full_benchmark),
|
| 232 |
+
"covers_core": bool(plan.covers_core),
|
| 233 |
+
"routing": {role: route.record() for role, route in plan.routes.items()},
|
| 234 |
+
# False when every answer was checked against the answer key, so the run scores the same on every judge's board.
|
| 235 |
+
"judge_used": bool(plan.judge_calls or plan.basic_tasks),
|
| 236 |
"status": run_status,
|
| 237 |
"resumed": resumed_from_existing,
|
| 238 |
"total_judged": total_judged,
|
src/elbench/execution/basic_education.py
CHANGED
|
@@ -7,6 +7,7 @@ import json
|
|
| 7 |
import logging
|
| 8 |
import os
|
| 9 |
import shutil
|
|
|
|
| 10 |
from dataclasses import dataclass
|
| 11 |
from datetime import datetime, timezone
|
| 12 |
from functools import reduce
|
|
@@ -131,6 +132,7 @@ class BasicEducationExecutor:
|
|
| 131 |
source_files: set[str] | None,
|
| 132 |
dimensions: set[str] | None,
|
| 133 |
max_samples: int | None,
|
|
|
|
| 134 |
) -> BasicEducationRunStats:
|
| 135 |
if not self.config.enabled:
|
| 136 |
raise ValueError(
|
|
@@ -154,6 +156,9 @@ class BasicEducationExecutor:
|
|
| 154 |
if max_remaining is not None and max_remaining <= 0:
|
| 155 |
self.logger.info("Reached max_samples limit for basic education branch.")
|
| 156 |
break
|
|
|
|
|
|
|
|
|
|
| 157 |
scenario_stats = await self._run_one_scenario(
|
| 158 |
scenario=scenario,
|
| 159 |
run_id=run_id,
|
|
@@ -162,7 +167,7 @@ class BasicEducationExecutor:
|
|
| 162 |
judged_writer=judged_writer,
|
| 163 |
failure_writer=failure_writer,
|
| 164 |
judge_enabled=judge_enabled,
|
| 165 |
-
max_samples=
|
| 166 |
)
|
| 167 |
stats.loaded_samples += scenario_stats.loaded_samples
|
| 168 |
stats.completed_samples += scenario_stats.completed_samples
|
|
@@ -512,7 +517,8 @@ class BasicEducationExecutor:
|
|
| 512 |
if not isinstance(extra_body, dict):
|
| 513 |
extra_body = {}
|
| 514 |
|
| 515 |
-
|
|
|
|
| 516 |
if key not in kargs:
|
| 517 |
continue
|
| 518 |
value = kargs.pop(key)
|
|
@@ -523,13 +529,15 @@ class BasicEducationExecutor:
|
|
| 523 |
|
| 524 |
def _tune_runtime_evaluator_payload(self, payload: dict[str, Any]) -> None:
|
| 525 |
kargs = payload.setdefault("kargs", {})
|
|
|
|
| 526 |
current_max_tokens = kargs.get("max_tokens")
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
|
|
|
| 533 |
extra_body = kargs.setdefault("extra_body", {})
|
| 534 |
if isinstance(extra_body, dict):
|
| 535 |
chat_template_kwargs = extra_body.setdefault("chat_template_kwargs", {})
|
|
@@ -548,7 +556,10 @@ class BasicEducationExecutor:
|
|
| 548 |
memory_path: Path | None = None,
|
| 549 |
expected_samples: int | None = None,
|
| 550 |
) -> None:
|
| 551 |
-
|
|
|
|
|
|
|
|
|
|
| 552 |
env = os.environ.copy()
|
| 553 |
cwd = str(self.project_config.project_root)
|
| 554 |
project_src = self.project_config.project_root / "src"
|
|
|
|
| 7 |
import logging
|
| 8 |
import os
|
| 9 |
import shutil
|
| 10 |
+
import sys
|
| 11 |
from dataclasses import dataclass
|
| 12 |
from datetime import datetime, timezone
|
| 13 |
from functools import reduce
|
|
|
|
| 132 |
source_files: set[str] | None,
|
| 133 |
dimensions: set[str] | None,
|
| 134 |
max_samples: int | None,
|
| 135 |
+
max_samples_per_file: int | None = None,
|
| 136 |
) -> BasicEducationRunStats:
|
| 137 |
if not self.config.enabled:
|
| 138 |
raise ValueError(
|
|
|
|
| 156 |
if max_remaining is not None and max_remaining <= 0:
|
| 157 |
self.logger.info("Reached max_samples limit for basic education branch.")
|
| 158 |
break
|
| 159 |
+
scenario_cap = max_remaining
|
| 160 |
+
if max_samples_per_file is not None:
|
| 161 |
+
scenario_cap = max_samples_per_file if scenario_cap is None else min(scenario_cap, max_samples_per_file)
|
| 162 |
scenario_stats = await self._run_one_scenario(
|
| 163 |
scenario=scenario,
|
| 164 |
run_id=run_id,
|
|
|
|
| 167 |
judged_writer=judged_writer,
|
| 168 |
failure_writer=failure_writer,
|
| 169 |
judge_enabled=judge_enabled,
|
| 170 |
+
max_samples=scenario_cap,
|
| 171 |
)
|
| 172 |
stats.loaded_samples += scenario_stats.loaded_samples
|
| 173 |
stats.completed_samples += scenario_stats.completed_samples
|
|
|
|
| 517 |
if not isinstance(extra_body, dict):
|
| 518 |
extra_body = {}
|
| 519 |
|
| 520 |
+
# "provider" is OpenRouter's host-routing field.
|
| 521 |
+
for key in ("chat_template_kwargs", "served_model_name", "provider"):
|
| 522 |
if key not in kargs:
|
| 523 |
continue
|
| 524 |
value = kargs.pop(key)
|
|
|
|
| 529 |
|
| 530 |
def _tune_runtime_evaluator_payload(self, payload: dict[str, Any]) -> None:
|
| 531 |
kargs = payload.setdefault("kargs", {})
|
| 532 |
+
# A card that sets a limit gets at least DEFAULT_EVALUATOR_MAX_TOKENS; a judge with no limit keeps none.
|
| 533 |
current_max_tokens = kargs.get("max_tokens")
|
| 534 |
+
if current_max_tokens is not None:
|
| 535 |
+
try:
|
| 536 |
+
max_tokens = int(current_max_tokens)
|
| 537 |
+
except (TypeError, ValueError):
|
| 538 |
+
max_tokens = 0
|
| 539 |
+
if max_tokens < DEFAULT_EVALUATOR_MAX_TOKENS:
|
| 540 |
+
kargs["max_tokens"] = DEFAULT_EVALUATOR_MAX_TOKENS
|
| 541 |
extra_body = kargs.setdefault("extra_body", {})
|
| 542 |
if isinstance(extra_body, dict):
|
| 543 |
chat_template_kwargs = extra_body.setdefault("chat_template_kwargs", {})
|
|
|
|
| 556 |
memory_path: Path | None = None,
|
| 557 |
expected_samples: int | None = None,
|
| 558 |
) -> None:
|
| 559 |
+
# "python" means this interpreter, so the runtime sees the same installed packages even when
|
| 560 |
+
# no bare `python` is on PATH (a venv started by full path, macOS with only python3).
|
| 561 |
+
runtime_python = sys.executable if self.config.runtime_python == "python" else self.config.runtime_python
|
| 562 |
+
command = [runtime_python, "-m", self.config.runtime_cli_module, *cli_args]
|
| 563 |
env = os.environ.copy()
|
| 564 |
cwd = str(self.project_config.project_root)
|
| 565 |
project_src = self.project_config.project_root / "src"
|
src/elbench/execution/runner.py
CHANGED
|
@@ -45,6 +45,8 @@ class RunOptions:
|
|
| 45 |
source_files: set[str] | None = None
|
| 46 |
dimensions: set[str] | None = None
|
| 47 |
max_samples: int | None = None
|
|
|
|
|
|
|
| 48 |
max_concurrency: int | None = None
|
| 49 |
resume: bool = True
|
| 50 |
judge_enabled: bool = True
|
|
@@ -130,6 +132,7 @@ class BenchmarkRunner:
|
|
| 130 |
source_files=options.source_files,
|
| 131 |
dimensions=options.dimensions,
|
| 132 |
max_samples=remaining_for_basic,
|
|
|
|
| 133 |
)
|
| 134 |
basic_stats = {
|
| 135 |
"scenario_count": basic_result.scenario_count,
|
|
@@ -182,7 +185,11 @@ class BenchmarkRunner:
|
|
| 182 |
subsets=options.subsets,
|
| 183 |
source_files=options.source_files,
|
| 184 |
)
|
| 185 |
-
samples = list(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
logger.info("Resolved %s samples from %s files.", len(samples), len(resolved_items))
|
| 187 |
if not samples:
|
| 188 |
return {"loaded_samples": 0, "failed_samples": 0}
|
|
@@ -251,15 +258,20 @@ class BenchmarkRunner:
|
|
| 251 |
resolved_items: Iterable,
|
| 252 |
dimensions: set[str] | None,
|
| 253 |
max_samples: int | None,
|
|
|
|
| 254 |
) -> Iterable[Sample]:
|
| 255 |
loaded = 0
|
| 256 |
for item in resolved_items:
|
| 257 |
loader = LoaderFactory.create(item.entry.loader_name)
|
|
|
|
| 258 |
for sample in loader.iter_samples(item):
|
| 259 |
if dimensions and sample.dimension not in dimensions:
|
| 260 |
continue
|
|
|
|
|
|
|
| 261 |
yield sample
|
| 262 |
loaded += 1
|
|
|
|
| 263 |
if max_samples is not None and loaded >= max_samples:
|
| 264 |
return
|
| 265 |
|
|
|
|
| 45 |
source_files: set[str] | None = None
|
| 46 |
dimensions: set[str] | None = None
|
| 47 |
max_samples: int | None = None
|
| 48 |
+
# Cap per source file (per scenario for basic education), applied before max_samples.
|
| 49 |
+
max_samples_per_file: int | None = None
|
| 50 |
max_concurrency: int | None = None
|
| 51 |
resume: bool = True
|
| 52 |
judge_enabled: bool = True
|
|
|
|
| 132 |
source_files=options.source_files,
|
| 133 |
dimensions=options.dimensions,
|
| 134 |
max_samples=remaining_for_basic,
|
| 135 |
+
max_samples_per_file=options.max_samples_per_file,
|
| 136 |
)
|
| 137 |
basic_stats = {
|
| 138 |
"scenario_count": basic_result.scenario_count,
|
|
|
|
| 185 |
subsets=options.subsets,
|
| 186 |
source_files=options.source_files,
|
| 187 |
)
|
| 188 |
+
samples = list(
|
| 189 |
+
self._load_samples(
|
| 190 |
+
resolved_items, options.dimensions, options.max_samples, options.max_samples_per_file
|
| 191 |
+
)
|
| 192 |
+
)
|
| 193 |
logger.info("Resolved %s samples from %s files.", len(samples), len(resolved_items))
|
| 194 |
if not samples:
|
| 195 |
return {"loaded_samples": 0, "failed_samples": 0}
|
|
|
|
| 258 |
resolved_items: Iterable,
|
| 259 |
dimensions: set[str] | None,
|
| 260 |
max_samples: int | None,
|
| 261 |
+
max_samples_per_file: int | None = None,
|
| 262 |
) -> Iterable[Sample]:
|
| 263 |
loaded = 0
|
| 264 |
for item in resolved_items:
|
| 265 |
loader = LoaderFactory.create(item.entry.loader_name)
|
| 266 |
+
from_file = 0
|
| 267 |
for sample in loader.iter_samples(item):
|
| 268 |
if dimensions and sample.dimension not in dimensions:
|
| 269 |
continue
|
| 270 |
+
if max_samples_per_file is not None and from_file >= max_samples_per_file:
|
| 271 |
+
break
|
| 272 |
yield sample
|
| 273 |
loaded += 1
|
| 274 |
+
from_file += 1
|
| 275 |
if max_samples is not None and loaded >= max_samples:
|
| 276 |
return
|
| 277 |
|
src/elbench/judges/llm_judge.py
CHANGED
|
@@ -78,11 +78,9 @@ class LLMJudge(BaseJudge):
|
|
| 78 |
raise KeyError(f"Unknown judge model_id={model_id!r}")
|
| 79 |
return model_id
|
| 80 |
|
| 81 |
-
def _judge_max_tokens(self) -> int:
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
return 1024
|
| 85 |
-
return min(1024, model_config.max_tokens)
|
| 86 |
|
| 87 |
|
| 88 |
def _coerce_score(value) -> float:
|
|
|
|
| 78 |
raise KeyError(f"Unknown judge model_id={model_id!r}")
|
| 79 |
return model_id
|
| 80 |
|
| 81 |
+
def _judge_max_tokens(self) -> int | None:
|
| 82 |
+
"""The judge card's own limit, or none: a reasoning judge cut off mid-thought returns no grade."""
|
| 83 |
+
return self._project_config.models[self._judge_model_id()].max_tokens
|
|
|
|
|
|
|
| 84 |
|
| 85 |
|
| 86 |
def _coerce_score(value) -> float:
|
tests/test_dashboard_faithfulness.py
CHANGED
|
@@ -696,6 +696,289 @@ class DashboardFaithfulnessTest(unittest.TestCase):
|
|
| 696 |
self.assertTrue(any("mmlu-full" in str(row[1]) for row in rows))
|
| 697 |
|
| 698 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
def _close_client(client: OpenAICompatibleClient) -> None:
|
| 700 |
import asyncio
|
| 701 |
|
|
|
|
| 696 |
self.assertTrue(any("mmlu-full" in str(row[1]) for row in rows))
|
| 697 |
|
| 698 |
|
| 699 |
+
def test_core_scope_drops_the_china_specific_sets_and_keeps_paper_scope_reproducible(self):
|
| 700 |
+
from elbench.dashboard.leaderboard import (
|
| 701 |
+
CHINA_SPECIFIC_FILES,
|
| 702 |
+
SCOPE_CORE,
|
| 703 |
+
SCOPE_PAPER,
|
| 704 |
+
load_official_models,
|
| 705 |
+
module_scores_from_per_file,
|
| 706 |
+
official_leaderboard_rows,
|
| 707 |
+
official_scores,
|
| 708 |
+
scope_item_count,
|
| 709 |
+
)
|
| 710 |
+
|
| 711 |
+
self.assertEqual(scope_item_count(SCOPE_PAPER), 2939)
|
| 712 |
+
self.assertEqual(scope_item_count(SCOPE_CORE), 2939 - 250 - 208)
|
| 713 |
+
for model in load_official_models():
|
| 714 |
+
paper = official_scores(model, SCOPE_PAPER)
|
| 715 |
+
self.assertEqual(paper["overall"], model["overall"])
|
| 716 |
+
core = official_scores(model, SCOPE_CORE)
|
| 717 |
+
kept = {fn: stats for fn, stats in model["per_file"].items() if fn not in CHINA_SPECIFIC_FILES}
|
| 718 |
+
self.assertEqual(core["overall"], module_scores_from_per_file(kept)["overall_pct"])
|
| 719 |
+
# High-Level and Basic Education hold no China-specific file, so they don't move.
|
| 720 |
+
self.assertAlmostEqual(core["high_level"], model["high_level"], delta=0.1)
|
| 721 |
+
self.assertAlmostEqual(core["basic_education"], model["basic_education"], delta=0.1)
|
| 722 |
+
core_rows = official_leaderboard_rows(SCOPE_CORE)
|
| 723 |
+
self.assertEqual([row[0] for row in core_rows], list(range(1, len(core_rows) + 1)))
|
| 724 |
+
self.assertEqual(core_rows[0][1], "GPT-5.4")
|
| 725 |
+
self.assertNotIn("[", core_rows[0][2]) # no published range outside the paper's scope
|
| 726 |
+
self.assertIn("[", official_leaderboard_rows(SCOPE_PAPER)[0][2])
|
| 727 |
+
|
| 728 |
+
def test_run_without_china_specific_sets_is_complete_and_plotted_in_core_scope_only(self):
|
| 729 |
+
from elbench.dashboard.leaderboard import (
|
| 730 |
+
CHINA_SPECIFIC_FILES,
|
| 731 |
+
SCOPE_CORE,
|
| 732 |
+
SCOPE_PAPER,
|
| 733 |
+
build_pareto_frontier,
|
| 734 |
+
full_file_counts,
|
| 735 |
+
run_coverage_label,
|
| 736 |
+
run_covers_metric,
|
| 737 |
+
)
|
| 738 |
+
|
| 739 |
+
counts = full_file_counts()
|
| 740 |
+
run = {
|
| 741 |
+
"run_id": "core-run",
|
| 742 |
+
"target_model": "qwen/qwen3-235b-a22b-2507",
|
| 743 |
+
"normalized_scores": {
|
| 744 |
+
"per_file": {
|
| 745 |
+
fn: {"count": n, "normalized_pct": 80.0}
|
| 746 |
+
for fn, n in counts.items()
|
| 747 |
+
if fn not in CHINA_SPECIFIC_FILES
|
| 748 |
+
}
|
| 749 |
+
},
|
| 750 |
+
}
|
| 751 |
+
self.assertTrue(run_covers_metric(run, "overall", counts, scope=SCOPE_CORE))
|
| 752 |
+
self.assertFalse(run_covers_metric(run, "overall", counts, scope=SCOPE_PAPER))
|
| 753 |
+
self.assertEqual(run_coverage_label(run, counts), "Complete, without the China-specific sets")
|
| 754 |
+
prices = {"qwen/qwen3-235b-a22b-2507": (9e-8, 5.5e-7)}
|
| 755 |
+
_, rows, _ = build_pareto_frontier([run], prices, scope=SCOPE_CORE)
|
| 756 |
+
self.assertTrue(any("core-run" in str(row[1]) for row in rows))
|
| 757 |
+
_, rows, _ = build_pareto_frontier([run], prices, scope=SCOPE_PAPER)
|
| 758 |
+
self.assertFalse(any("core-run" in str(row[1]) for row in rows))
|
| 759 |
+
|
| 760 |
+
def test_plan_reports_core_coverage(self):
|
| 761 |
+
from elbench.dashboard.leaderboard import CHINA_SPECIFIC_FILES
|
| 762 |
+
|
| 763 |
+
core_files = {e.canonical_name for e in self.catalog.files} - set(CHINA_SPECIFIC_FILES)
|
| 764 |
+
common = dict(
|
| 765 |
+
openrouter_model="openai/gpt-5.4",
|
| 766 |
+
modules=set(self.catalog.modules()),
|
| 767 |
+
dimensions=set(),
|
| 768 |
+
max_samples=None,
|
| 769 |
+
prompt_usd_per_token=1e-6,
|
| 770 |
+
completion_usd_per_token=2e-6,
|
| 771 |
+
max_concurrency=8,
|
| 772 |
+
)
|
| 773 |
+
core = build_plan(self.catalog, source_files=core_files, **common)
|
| 774 |
+
self.assertTrue(core.covers_core)
|
| 775 |
+
self.assertFalse(core.is_full_benchmark)
|
| 776 |
+
capped = build_plan(self.catalog, source_files=core_files, max_samples_per_file=1, **common)
|
| 777 |
+
self.assertFalse(capped.covers_core)
|
| 778 |
+
|
| 779 |
+
def test_quick_check_takes_one_question_from_each_task(self):
|
| 780 |
+
files = {"mmlu_pro_sampled.jsonl", "ifeval_sampled.jsonl", "math_500_sampled.jsonl"}
|
| 781 |
+
plan = build_plan(
|
| 782 |
+
self.catalog,
|
| 783 |
+
openrouter_model="openai/gpt-5.4",
|
| 784 |
+
modules={"通用模型"},
|
| 785 |
+
source_files=files,
|
| 786 |
+
dimensions=set(),
|
| 787 |
+
max_samples=None,
|
| 788 |
+
max_samples_per_file=1,
|
| 789 |
+
prompt_usd_per_token=1e-6,
|
| 790 |
+
completion_usd_per_token=2e-6,
|
| 791 |
+
max_concurrency=8,
|
| 792 |
+
)
|
| 793 |
+
self.assertEqual(plan.standard_requests, 3)
|
| 794 |
+
self.assertEqual(sorted(name for name, _ in plan.file_counts), sorted(files))
|
| 795 |
+
self.assertTrue(all(count == 1 for _, count in plan.file_counts))
|
| 796 |
+
resolved = FileRegistry(self.catalog.config).resolve(modules={"通用模型"}, source_files=files)
|
| 797 |
+
samples = list(BenchmarkRunner(self.catalog.config)._load_samples(resolved, None, None, 1))
|
| 798 |
+
self.assertEqual(sorted(s.source_file for s in samples), sorted(files))
|
| 799 |
+
teaching = build_plan(
|
| 800 |
+
self.catalog,
|
| 801 |
+
openrouter_model="openai/gpt-5.4",
|
| 802 |
+
modules={"基本教育"},
|
| 803 |
+
source_files={"knowledge.yaml", "cross.yaml"},
|
| 804 |
+
dimensions=set(),
|
| 805 |
+
max_samples=None,
|
| 806 |
+
max_samples_per_file=1,
|
| 807 |
+
prompt_usd_per_token=1e-6,
|
| 808 |
+
completion_usd_per_token=2e-6,
|
| 809 |
+
max_concurrency=8,
|
| 810 |
+
)
|
| 811 |
+
self.assertEqual(teaching.basic_tasks, 2)
|
| 812 |
+
|
| 813 |
+
def test_visitor_judge_and_student_have_no_length_limit(self):
|
| 814 |
+
from elbench.dashboard.protocol import build_judge_model_config, build_multiturn_chat_model_config
|
| 815 |
+
from elbench.execution.basic_education import BasicEducationExecutor
|
| 816 |
+
|
| 817 |
+
config = self.catalog.config
|
| 818 |
+
# A listed card sets max_tokens for answers; the judge and student copies must not inherit a cap.
|
| 819 |
+
judge = build_judge_model_config(config, "openai/gpt-5.4")
|
| 820 |
+
student = build_multiturn_chat_model_config(config, "openai/gpt-5.4")
|
| 821 |
+
self.assertIsNone(judge.max_tokens)
|
| 822 |
+
self.assertIsNone(student.max_tokens)
|
| 823 |
+
payload = {"kargs": {}}
|
| 824 |
+
BasicEducationExecutor._tune_runtime_evaluator_payload(None, payload)
|
| 825 |
+
self.assertNotIn("max_tokens", payload["kargs"])
|
| 826 |
+
capped = {"kargs": {"max_tokens": 512}}
|
| 827 |
+
BasicEducationExecutor._tune_runtime_evaluator_payload(None, capped)
|
| 828 |
+
self.assertEqual(capped["kargs"]["max_tokens"], 1024) # a card's own limit keeps its floor
|
| 829 |
+
|
| 830 |
+
def test_partial_visitor_run_shows_no_headline_score(self):
|
| 831 |
+
from elbench.dashboard.leaderboard import SCOPE_CORE, SCOPE_PAPER, custom_leaderboard_rows
|
| 832 |
+
|
| 833 |
+
quick = {
|
| 834 |
+
"run_id": "quick",
|
| 835 |
+
"target_model": "meta/muse-spark-1.3-contributor",
|
| 836 |
+
"total_judged": 5,
|
| 837 |
+
"normalized_scores": {
|
| 838 |
+
"overall_pct": 21.84,
|
| 839 |
+
"safety_pct": 21.84,
|
| 840 |
+
"per_file": {"安全拒答.jsonl": {"count": 5, "normalized_pct": 21.84}},
|
| 841 |
+
},
|
| 842 |
+
}
|
| 843 |
+
for scope in (SCOPE_CORE, SCOPE_PAPER):
|
| 844 |
+
row = custom_leaderboard_rows([quick], scope)[0]
|
| 845 |
+
self.assertEqual(row[5], "Part of the benchmark")
|
| 846 |
+
self.assertEqual(row[6], 5)
|
| 847 |
+
self.assertEqual(row[8:], [None] * 5, scope)
|
| 848 |
+
|
| 849 |
+
def test_route_keeps_the_highest_precision_and_sorts_by_price(self):
|
| 850 |
+
from elbench.dashboard.routing import route_from_endpoints
|
| 851 |
+
|
| 852 |
+
def host(name, quant, prompt, tier=None):
|
| 853 |
+
return {
|
| 854 |
+
"provider_name": name,
|
| 855 |
+
"quantization": quant,
|
| 856 |
+
"pricing": {"prompt": str(prompt), "completion": str(prompt * 4)},
|
| 857 |
+
"service_tier": tier,
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
mixed = route_from_endpoints(
|
| 861 |
+
"z-ai/glm-5.3-flash",
|
| 862 |
+
[host("A", "fp4", 1e-8), host("B", "unknown", 2e-8), host("C", "fp8", 5e-8), host("D", "fp8", 4e-8)],
|
| 863 |
+
)
|
| 864 |
+
self.assertEqual(mixed.provider, {"sort": "price", "quantizations": ["fp8"]})
|
| 865 |
+
self.assertEqual(mixed.cheapest.name, "D")
|
| 866 |
+
self.assertEqual(sorted(h.name for h in mixed.hosts), ["C", "D"])
|
| 867 |
+
half = route_from_endpoints("m", [host("A", "fp16", 3e-8), host("B", "bf16", 2e-8), host("C", "fp8", 1e-8)])
|
| 868 |
+
self.assertEqual(half.provider["quantizations"], ["bf16", "fp16"])
|
| 869 |
+
# Closed models report no precision: price order only, and opt-in priority tiers are never counted.
|
| 870 |
+
closed = route_from_endpoints(
|
| 871 |
+
"openai/gpt-6-luna", [host("OpenAI", "unknown", 1e-7), host("OpenAI", "unknown", 2e-8, "priority")]
|
| 872 |
+
)
|
| 873 |
+
self.assertEqual(closed.provider, {"sort": "price"})
|
| 874 |
+
self.assertEqual(closed.cheapest.prompt_usd_per_token, 1e-7)
|
| 875 |
+
self.assertNotIn("sort", route_from_endpoints("m:nitro", [host("A", "fp8", 1e-8)]).provider)
|
| 876 |
+
|
| 877 |
+
def test_plan_sends_the_route_with_every_chat_model(self):
|
| 878 |
+
from elbench.dashboard.routing import route_from_endpoints
|
| 879 |
+
from elbench.execution.basic_education import BasicEducationExecutor
|
| 880 |
+
|
| 881 |
+
fp8 = [{"provider_name": "A", "quantization": "fp8", "pricing": {"prompt": "1e-8", "completion": "4e-8"}}]
|
| 882 |
+
routes = {role: route_from_endpoints(f"{role}/model", fp8) for role in ("target", "judge", "student")}
|
| 883 |
+
plan = build_plan(
|
| 884 |
+
self.catalog,
|
| 885 |
+
openrouter_model="openai/gpt-5.4",
|
| 886 |
+
modules={"基本教育"},
|
| 887 |
+
source_files={"config_guided_task.yaml"},
|
| 888 |
+
dimensions=set(),
|
| 889 |
+
max_samples=None,
|
| 890 |
+
max_samples_per_file=1,
|
| 891 |
+
prompt_usd_per_token=1e-6,
|
| 892 |
+
completion_usd_per_token=2e-6,
|
| 893 |
+
max_concurrency=8,
|
| 894 |
+
judge_openrouter_model="z-ai/glm-5.3-flash",
|
| 895 |
+
use_multiturn_chat_model=True,
|
| 896 |
+
multiturn_openrouter_model="liquid/lfm-2.5-24b-a2b",
|
| 897 |
+
routes=routes,
|
| 898 |
+
)
|
| 899 |
+
expected = {"sort": "price", "quantizations": ["fp8"]}
|
| 900 |
+
self.assertEqual(set(plan.routes), {"target", "judge", "student"})
|
| 901 |
+
body = preview_standard_payload(self.catalog.config, plan.protocol.model_config, "hi")
|
| 902 |
+
self.assertEqual(body["provider"], expected)
|
| 903 |
+
judge_body = preview_standard_payload(self.catalog.config, plan.judge_model_config, "hi")
|
| 904 |
+
self.assertEqual(judge_body["provider"], expected)
|
| 905 |
+
# The teaching runtime calls LangChain's OpenAI client, which needs the field inside extra_body.
|
| 906 |
+
executor = BasicEducationExecutor.__new__(BasicEducationExecutor)
|
| 907 |
+
runtime = executor._build_runtime_model_payload(plan.multiturn_model_config)
|
| 908 |
+
self.assertEqual(runtime["kargs"]["extra_body"]["provider"], expected)
|
| 909 |
+
self.assertNotIn("provider", runtime["kargs"])
|
| 910 |
+
self.assertTrue(any(c.code == "openrouter_host_choice" for c in plan.caveats))
|
| 911 |
+
|
| 912 |
+
decision = build_plan(
|
| 913 |
+
self.catalog,
|
| 914 |
+
openrouter_model="openai/gpt-5.4",
|
| 915 |
+
modules={"安全可信"},
|
| 916 |
+
source_files={"安全拒答.jsonl"},
|
| 917 |
+
dimensions=set(),
|
| 918 |
+
max_samples=1,
|
| 919 |
+
prompt_usd_per_token=1e-6,
|
| 920 |
+
completion_usd_per_token=2e-6,
|
| 921 |
+
max_concurrency=8,
|
| 922 |
+
judge_openrouter_model="liquid/d1-ultra",
|
| 923 |
+
use_system_one_judge=True,
|
| 924 |
+
routes=routes,
|
| 925 |
+
)
|
| 926 |
+
self.assertNotIn("judge", decision.routes)
|
| 927 |
+
self.assertNotIn("provider", decision.judge_model_config.provider_kwargs)
|
| 928 |
+
|
| 929 |
+
def test_each_judge_has_its_own_board(self):
|
| 930 |
+
from elbench.dashboard.leaderboard import (
|
| 931 |
+
CHINA_SPECIFIC_FILES,
|
| 932 |
+
PAPER_JUDGE_SLUG,
|
| 933 |
+
PUBLISHED_SOURCE,
|
| 934 |
+
SCOPE_CORE,
|
| 935 |
+
build_comparison_table,
|
| 936 |
+
build_pareto_frontier,
|
| 937 |
+
full_file_counts,
|
| 938 |
+
judge_board_choices,
|
| 939 |
+
)
|
| 940 |
+
|
| 941 |
+
counts = full_file_counts()
|
| 942 |
+
per_file = {fn: {"count": n, "normalized_pct": 80.0} for fn, n in counts.items() if fn not in CHINA_SPECIFIC_FILES}
|
| 943 |
+
|
| 944 |
+
def run(rid, judge, **extra):
|
| 945 |
+
return {
|
| 946 |
+
"run_id": rid,
|
| 947 |
+
"target_model": "openai/gpt-6-luna",
|
| 948 |
+
"judge_model": judge,
|
| 949 |
+
"normalized_scores": {"per_file": per_file},
|
| 950 |
+
**extra,
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
default = run("default-run", PAPER_JUDGE_SLUG)
|
| 954 |
+
glm = run("glm-run", "z-ai/glm-5.3-flash")
|
| 955 |
+
no_judge = run("auto-run", "z-ai/glm-5.3-flash", judge_used=False)
|
| 956 |
+
decision = run("d1-run", "liquid/d1-ultra", use_system_one_judge=True)
|
| 957 |
+
runs = [default, glm, no_judge, decision]
|
| 958 |
+
prices = {"openai/gpt-6-luna": (1e-7, 5e-7)}
|
| 959 |
+
|
| 960 |
+
def names(board):
|
| 961 |
+
_, rows, md = build_pareto_frontier(runs, prices, scope=SCOPE_CORE, judge=board)
|
| 962 |
+
return {str(row[1]).split("(")[-1].rstrip(")") for row in rows if row[2] != PUBLISHED_SOURCE}, rows, md
|
| 963 |
+
|
| 964 |
+
shown, rows, _ = names(PAPER_JUDGE_SLUG)
|
| 965 |
+
self.assertEqual(shown, {"default-run", "auto-run"})
|
| 966 |
+
self.assertTrue(any(row[2] == PUBLISHED_SOURCE for row in rows))
|
| 967 |
+
shown, rows, md = names("z-ai/glm-5.3-flash")
|
| 968 |
+
self.assertEqual(shown, {"glm-run", "auto-run"})
|
| 969 |
+
self.assertFalse(any(row[2] == PUBLISHED_SOURCE for row in rows))
|
| 970 |
+
self.assertIn("Graded by z-ai/glm-5.3-flash", md)
|
| 971 |
+
shown, _, _ = names("decision:liquid/d1-ultra")
|
| 972 |
+
self.assertEqual(shown, {"d1-run", "auto-run"})
|
| 973 |
+
|
| 974 |
+
keys = [key for _, key in judge_board_choices(runs)]
|
| 975 |
+
self.assertEqual(keys, [PAPER_JUDGE_SLUG, "decision:liquid/d1-ultra", "z-ai/glm-5.3-flash"])
|
| 976 |
+
_, _, note = build_comparison_table("run:glm-run", ["official:gpt-5.4"], "By Module (0–100 Normalized)", runs)
|
| 977 |
+
self.assertIn("graded by different judges", note)
|
| 978 |
+
_, _, note = build_comparison_table("run:default-run", ["official:gpt-5.4"], "By Module (0–100 Normalized)", runs)
|
| 979 |
+
self.assertNotIn("different judges", note)
|
| 980 |
+
|
| 981 |
+
|
| 982 |
def _close_client(client: OpenAICompatibleClient) -> None:
|
| 983 |
import asyncio
|
| 984 |
|