adelevett commited on
Commit
68670f0
·
verified ·
1 Parent(s): e1a2929

Redeploy ELBench-live

Browse files
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 = "qwen/qwen3-235b-a22b-2507"
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
- QUICK_CHECK_ITEMS = 5
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 _preset_choices() -> list[tuple[str, str]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  if CATALOG is None:
351
  return []
352
- auto_items = sum(entry.item_count for entry in CATALOG.default_files())
353
- full_items = sum(entry.item_count for entry in CATALOG.files)
 
354
  return [
355
- (f"Quick check · {QUICK_CHECK_ITEMS} questions, to see that everything works", PRESET_QUICK),
356
  (f"Automatically checked set · {auto_items:,} questions, no judge model needed", PRESET_AUTO),
357
- (f"Full benchmark · all {full_items:,} questions · appears on the leaderboard chart", PRESET_FULL),
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
- if preset == PRESET_FULL:
365
- files = list(CATALOG.files)
366
- modules = CATALOG.modules()
367
- cap = None
368
- else:
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=cap),
 
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
- prompt_price, completion_price = _price_pair(prices, str(model_id).strip())
500
- judge_prompt_price, judge_completion_price = _price_pair(prices, str(judge_model_id).strip())
501
- mt_prompt_price, mt_completion_price = _price_pair(prices, str(multiturn_model_id or "").strip())
 
 
 
 
 
 
 
 
 
 
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.is_full_benchmark:
613
- lines.append("This is a full run: once it finishes, it appears on the leaderboard chart next to the published models.")
 
 
 
 
 
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
- if status_label == "completed" and (result.get("leaderboard_record") or {}).get("is_full_benchmark"):
879
- lines[-1] += " It now appears on the leaderboard chart; press **Refresh** there to see it."
 
 
 
 
 
 
 
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, single_task, cost_metric, include_visitor_runs, max_price, max_size, include_undisclosed, prices
 
 
 
 
 
 
 
 
 
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(resolved_primary, resolved_baselines, granularity, runs)
 
 
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 update_comparison(primary_sel, baseline_sels, granularity):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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. When two models' ranges
996
- overlap, the difference between them could disappear on a re-run, and the chart says so.
 
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, which may route a request to any provider that serves the model.
 
 
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 slightly from the published ones.
 
 
 
 
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, initial_prices
 
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 = CATALOG.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 2,939 questions in four areas: **teaching**, **safety**, "
1091
- "**knowledge & reasoning**, and **character & thinking skills**. Each dot is a model: "
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 (all 2,939 questions)", open=False):
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 judge.",
 
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=CATALOG.default_modules(),
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=QUICK_CHECK_ITEMS,
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
- [chart, value_table, takeaway, runs_table, runs_status, comp_primary, comp_baselines, comp_table, comp_note],
 
 
 
 
 
 
 
 
 
 
 
1348
  )
1349
  go_test.click(lambda: gr.Tabs(selected="test"), None, tabs)
1350
- official_model_filter.change(official_per_file_rows, [official_model_filter], [official_file_table])
 
 
 
 
1351
  for control in (comp_primary, comp_baselines, comp_granularity):
1352
- control.change(update_comparison, [comp_primary, comp_baselines, comp_granularity], [comp_table, comp_note])
 
 
 
 
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: Harmful-Request Refusal",
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 (Harmful-Request Refusal)",
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: Harmful-Request Refusal",
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: ``Harmful-Request Refusal``."""
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 load_official_models():
 
 
 
75
  rows.append(
76
  [
77
- item["rank"],
78
  item["display_name"],
79
- f"{item['overall']:.1f} {item['ci_95']}",
80
- round(float(item["general"]), 1),
81
- round(float(item["safety"]), 1),
82
- round(float(item["basic_education"]), 1),
83
- round(float(item["high_level"]), 1),
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 custom_leaderboard_rows(runs: list[dict[str, Any]]) -> list[list[Any]]:
520
- """Format persisted custom runs for the Live & Custom Leaderboard Dataframe."""
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  rows: list[list[Any]] = []
522
  for run in runs:
523
- norm = run.get("normalized_scores") or {}
 
 
 
 
 
 
 
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
- scope_label,
536
  run.get("total_judged", 0),
537
  run.get("total_failures", 0),
538
- norm.get("overall_pct"),
539
- norm.get("general_pct"),
540
- norm.get("safety_pct"),
541
- norm.get("basic_education_pct"),
542
- norm.get("high_level_pct"),
543
  ]
544
  )
545
  return rows
546
 
547
 
548
- def build_comparison_choices(runs: list[dict[str, Any]]) -> tuple[list[tuple[str, str]], list[tuple[str, str]]]:
 
 
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 = (run.get("normalized_scores") or {}).get("overall_pct")
557
- score_str = f"{overall:.1f}%" if isinstance(overall, (int, float)) else "n/a"
 
 
 
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
- label = (
564
- f"Published: #{item['rank']} {item['display_name']} "
565
- f"(Overall {item['overall']:.1f} {item['ci_95']})"
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(selector: str, runs_by_id: dict[str, dict[str, Any]], official_by_id: dict[str, dict[str, Any]]) -> dict[str, Any] | None:
 
 
 
 
 
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
- "overall": norm.get("overall_pct"),
586
- "general": norm.get("general_pct"),
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
- "overall": off.get("overall"),
601
- "general": off.get("general"),
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
- active_files = [fn for fn in FILE_TO_MODULE if fn in primary_files and int(primary_files[fn].get("count") or 0) > 0]
 
682
  if not active_files:
683
- active_files = list(FILE_TO_MODULE.keys())
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(run: dict[str, Any], score_metric: str, counts: dict[str, int] | None = None) -> bool:
790
- """True when the run scored every item behind ``score_metric``.
 
 
 
 
 
 
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
- files = METRIC_SCOPE_FILES.get(score_metric, tuple(FILE_TO_MODULE))
 
 
 
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
- for off in load_official_models():
 
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
- "ci": official_ci(off, score_metric),
 
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
- norm = run.get("normalized_scores") or {}
930
- mapped = {
931
- "overall": norm.get("overall_pct"),
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
- "**Best value:** "
1033
- + (" → ".join(frontier_names) if frontier_names else "none with a price yet")
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 1024
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 load-balances providers of this model slug by default. "
253
- "allow_fallbacks defaults to true, so a provider error is retried on another "
254
- "provider of the same slug. require_parameters defaults to false, so a provider "
255
- "that does not implement temperature or max_tokens still accepts the request and "
256
- "ignores those fields. This app does not send a provider object or a models "
257
- "fallback list. 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,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
- "max_tokens": min(1024, int(protocol.model_config.max_tokens or 1024)),
 
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
- "max_tokens": min(512, int(protocol.model_config.max_tokens or 512)),
 
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=max_remaining,
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
- for key in ("chat_template_kwargs", "served_model_name"):
 
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
- try:
528
- max_tokens = int(current_max_tokens)
529
- except (TypeError, ValueError):
530
- max_tokens = 0
531
- if max_tokens < DEFAULT_EVALUATOR_MAX_TOKENS:
532
- kargs["max_tokens"] = DEFAULT_EVALUATOR_MAX_TOKENS
 
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
- command = [self.config.runtime_python, "-m", self.config.runtime_cli_module, *cli_args]
 
 
 
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(self._load_samples(resolved_items, options.dimensions, options.max_samples))
 
 
 
 
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
- model_config = self._project_config.models[self._judge_model_id()]
83
- if model_config.max_tokens is None:
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