benchkit-cli 0.1.2__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/PKG-INFO +3 -1
  2. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/README.md +2 -0
  3. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/pyproject.toml +1 -1
  4. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/__init__.py +14 -0
  5. benchkit_cli-0.2.1/src/benchkit/benchmarks/cruxeval.py +380 -0
  6. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/patcheval.py +21 -4
  7. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/ruler_generator.py +47 -17
  8. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/README.md +14 -0
  9. benchkit_cli-0.2.1/src/benchkit/datasets/cruxeval.jsonl +800 -0
  10. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/executor.py +12 -2
  11. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit_cli.egg-info/PKG-INFO +3 -1
  12. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit_cli.egg-info/SOURCES.txt +3 -0
  13. benchkit_cli-0.2.1/tests/test_cruxeval.py +139 -0
  14. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_patcheval.py +26 -1
  15. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_ruler.py +34 -0
  16. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/LICENSE +0 -0
  17. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/setup.cfg +0 -0
  18. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/__init__.py +0 -0
  19. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/_pi_proxy.py +0 -0
  20. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/answer_key_guard.ts +0 -0
  21. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/artifacts.py +0 -0
  22. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/aider_polyglot.py +0 -0
  23. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/arc.py +0 -0
  24. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/banking77.py +0 -0
  25. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/base.py +0 -0
  26. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/boolq.py +0 -0
  27. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/chartqa.py +0 -0
  28. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/code_verifier.py +0 -0
  29. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/downloads.py +0 -0
  30. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/evalplus.py +0 -0
  31. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/git_surgery.py +0 -0
  32. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/gpqa.py +0 -0
  33. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/gsm8k.py +0 -0
  34. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/hellaswag.py +0 -0
  35. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/humaneval.py +0 -0
  36. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/ifeval.py +0 -0
  37. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/ifeval_instructions.py +0 -0
  38. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/mbpp.py +0 -0
  39. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/mc_arena.py +0 -0
  40. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/mcq.py +0 -0
  41. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/medfailbench.py +0 -0
  42. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/mmlu.py +0 -0
  43. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/mmlu_pro.py +0 -0
  44. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/mmvp.py +0 -0
  45. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/ocrbench.py +0 -0
  46. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/openbookqa.py +0 -0
  47. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/piqa.py +0 -0
  48. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/ruler.py +0 -0
  49. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/sanity.py +0 -0
  50. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/treejs_arena.py +0 -0
  51. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/truthfulqa.py +0 -0
  52. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/utils.py +0 -0
  53. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/vision.py +0 -0
  54. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/vstar.py +0 -0
  55. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/winogrande.py +0 -0
  56. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/benchmarks/xstest.py +0 -0
  57. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/browser.py +0 -0
  58. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/catalog.py +0 -0
  59. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/cli.py +0 -0
  60. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/client.py +0 -0
  61. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/config.py +0 -0
  62. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/controls.py +0 -0
  63. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/MMLU_LICENSE.txt +0 -0
  64. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/arc_challenge.jsonl +0 -0
  65. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/banking77.jsonl +0 -0
  66. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/banking77_intents.json +0 -0
  67. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/boolq.jsonl +0 -0
  68. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/code_verifier.jsonl +0 -0
  69. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/gpqa_diamond.jsonl +0 -0
  70. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/gsm8k.jsonl +0 -0
  71. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/hellaswag.jsonl +0 -0
  72. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/humaneval.jsonl +0 -0
  73. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/ifeval.jsonl +0 -0
  74. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/mbpp.jsonl +0 -0
  75. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/mc_blocks_1_20_1.jsonl +0 -0
  76. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/medfailbench_hard30.jsonl +0 -0
  77. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/medfailbench_hard30.md +0 -0
  78. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/openbookqa.jsonl +0 -0
  79. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/piqa.jsonl +0 -0
  80. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/sanity_code.jsonl +0 -0
  81. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/truthfulqa.jsonl +0 -0
  82. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/winogrande.jsonl +0 -0
  83. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/datasets/xstest.csv +0 -0
  84. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/decision.py +0 -0
  85. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/demo.py +0 -0
  86. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/dockerfiles.py +0 -0
  87. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/engine.py +0 -0
  88. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/evaluation.py +0 -0
  89. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/events.py +0 -0
  90. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/backport-release-stack/setup.sh +0 -0
  91. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/backport-release-stack/verify.sh +0 -0
  92. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/bisect-the-regression/setup.sh +0 -0
  93. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/bisect-the-regression/verify.sh +0 -0
  94. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/rebase-conflict-chain/setup.sh +0 -0
  95. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/rebase-conflict-chain/verify.sh +0 -0
  96. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/recover-complex-stash/setup.sh +0 -0
  97. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/recover-complex-stash/verify.sh +0 -0
  98. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/recover-lost-work/setup.sh +0 -0
  99. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/recover-lost-work/verify.sh +0 -0
  100. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/repair-force-pushed-remote/setup.sh +0 -0
  101. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/repair-force-pushed-remote/verify.sh +0 -0
  102. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/revert-merge-with-followups/setup.sh +0 -0
  103. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/revert-merge-with-followups/verify.sh +0 -0
  104. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/secret-in-history/README.md +0 -0
  105. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/secret-in-history/setup.sh +0 -0
  106. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/secret-in-history/verify.sh +0 -0
  107. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/split-the-mega-commit/setup.sh +0 -0
  108. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/split-the-mega-commit/verify.sh +0 -0
  109. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/untangle-nested-submodules/setup.sh +0 -0
  110. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/git_surgery/untangle-nested-submodules/verify.sh +0 -0
  111. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/history.py +0 -0
  112. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/jobs.py +0 -0
  113. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/leakage.py +0 -0
  114. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/looping.py +0 -0
  115. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/mc_render.py +0 -0
  116. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/mc_viewer/dist/atlas.png +0 -0
  117. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
  118. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/mc_viewer/dist/version.json +0 -0
  119. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/mc_viewer/dist/viewer.html +0 -0
  120. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/mc_viewer/dist/viewer.js +0 -0
  121. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +0 -0
  122. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/mc_viewer/dist/worker.js +0 -0
  123. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/mc_viewer/dist/worker.js.LICENSE.txt +0 -0
  124. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/metrics.py +0 -0
  125. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/ollama.py +0 -0
  126. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/perf.py +0 -0
  127. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/perf_report.py +0 -0
  128. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/perturbations.py +0 -0
  129. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/pi_agent.py +0 -0
  130. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/pi_package/package-lock.json +0 -0
  131. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/pi_package/package.json +0 -0
  132. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/report.py +0 -0
  133. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/report_markdown.py +0 -0
  134. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/results.py +0 -0
  135. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/retry.py +0 -0
  136. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/runner.py +0 -0
  137. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/sandbox.py +0 -0
  138. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/template_check.py +0 -0
  139. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/templates/arena.html +0 -0
  140. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/templates/history.html +0 -0
  141. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/templates/perf.html +0 -0
  142. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/templates/report.html +0 -0
  143. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/__init__.py +0 -0
  144. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/app.py +0 -0
  145. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/app.tcss +0 -0
  146. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/formatting.py +0 -0
  147. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/screens/__init__.py +0 -0
  148. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/screens/connect.py +0 -0
  149. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/screens/detail.py +0 -0
  150. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/screens/help.py +0 -0
  151. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/screens/modals.py +0 -0
  152. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/screens/results.py +0 -0
  153. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/screens/run.py +0 -0
  154. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/screens/setup.py +0 -0
  155. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/theme.py +0 -0
  156. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit/tui/widgets.py +0 -0
  157. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit_cli.egg-info/dependency_links.txt +0 -0
  158. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit_cli.egg-info/entry_points.txt +0 -0
  159. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit_cli.egg-info/requires.txt +0 -0
  160. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/src/benchkit_cli.egg-info/top_level.txt +0 -0
  161. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_aider_polyglot.py +0 -0
  162. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_banking77.py +0 -0
  163. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_code_verifier.py +0 -0
  164. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_concurrency.py +0 -0
  165. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_config.py +0 -0
  166. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_datasets.py +0 -0
  167. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_decision.py +0 -0
  168. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_evalplus.py +0 -0
  169. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_git_surgery.py +0 -0
  170. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_gsm8k.py +0 -0
  171. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_history.py +0 -0
  172. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_humaneval.py +0 -0
  173. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_looping.py +0 -0
  174. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_mc_arena.py +0 -0
  175. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_mcq.py +0 -0
  176. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_medfailbench.py +0 -0
  177. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_medfailbench_real_outputs.py +0 -0
  178. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_medfailbench_real_outputs_escalation.py +0 -0
  179. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_medfailbench_scoring_distortion.py +0 -0
  180. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_perturbations.py +0 -0
  181. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_pi_harness.py +0 -0
  182. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_repair.py +0 -0
  183. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_report.py +0 -0
  184. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_retry.py +0 -0
  185. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_runner.py +0 -0
  186. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_sanity.py +0 -0
  187. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_treejs_arena.py +0 -0
  188. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_unload.py +0 -0
  189. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_utils.py +0 -0
  190. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_vision.py +0 -0
  191. {benchkit_cli-0.1.2 → benchkit_cli-0.2.1}/tests/test_xstest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: benchkit-cli
3
- Version: 0.1.2
3
+ Version: 0.2.1
4
4
  Summary: Benchmark your local LLMs with real eval suites
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -139,6 +139,8 @@ uv run benchkit --help
139
139
  | HumanEval+ TR | `humaneval-plus-tr` | 164 | HumanEval+ fully translated to Turkish, function and variable names included |
140
140
  | MBPP | `mbpp` | 500 | Short Python programming tasks |
141
141
  | MBPP+ | `mbpp-plus` | 378 | MBPP with expanded EvalPlus tests |
142
+ | CRUXEval-O | `cruxeval-o` | 800 | Predict what a short Python function returns (`-cot` for the step-by-step prompt) |
143
+ | CRUXEval-I | `cruxeval-i` | 800 | Find an input that produces a given output, checked by running it (`-cot` too) |
142
144
  | GSM8K | `gsm8k` | 1,319 | Multi-step grade-school math |
143
145
  | IFEval | `ifeval` | 541 | Verifiable instruction following |
144
146
  | RULER | `ruler` | 39 × 6 | Practical long-context retrieval from 4K to 128K |
@@ -121,6 +121,8 @@ uv run benchkit --help
121
121
  | HumanEval+ TR | `humaneval-plus-tr` | 164 | HumanEval+ fully translated to Turkish, function and variable names included |
122
122
  | MBPP | `mbpp` | 500 | Short Python programming tasks |
123
123
  | MBPP+ | `mbpp-plus` | 378 | MBPP with expanded EvalPlus tests |
124
+ | CRUXEval-O | `cruxeval-o` | 800 | Predict what a short Python function returns (`-cot` for the step-by-step prompt) |
125
+ | CRUXEval-I | `cruxeval-i` | 800 | Find an input that produces a given output, checked by running it (`-cot` too) |
124
126
  | GSM8K | `gsm8k` | 1,319 | Multi-step grade-school math |
125
127
  | IFEval | `ifeval` | 541 | Verifiable instruction following |
126
128
  | RULER | `ruler` | 39 × 6 | Practical long-context retrieval from 4K to 128K |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "benchkit-cli"
3
- version = "0.1.2"
3
+ version = "0.2.1"
4
4
  description = "Benchmark your local LLMs with real eval suites"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -6,6 +6,12 @@ from benchkit.benchmarks.banking77 import Banking77
6
6
  from benchkit.benchmarks.boolq import BoolQ
7
7
  from benchkit.benchmarks.chartqa import ChartQA
8
8
  from benchkit.benchmarks.code_verifier import CodeVerifier
9
+ from benchkit.benchmarks.cruxeval import (
10
+ CRUXEvalInput,
11
+ CRUXEvalInputCoT,
12
+ CRUXEvalOutput,
13
+ CRUXEvalOutputCoT,
14
+ )
9
15
  from benchkit.benchmarks.evalplus import HumanEvalPlus, HumanEvalPlusTR, MBPPPlus
10
16
  from benchkit.benchmarks.git_surgery import GitSurgery
11
17
  from benchkit.benchmarks.gpqa import GPQA
@@ -44,6 +50,10 @@ REGISTRY: dict[str, type] = {
44
50
  "humaneval-plus-tr": HumanEvalPlusTR,
45
51
  "mbpp": MBPP,
46
52
  "mbpp-plus": MBPPPlus,
53
+ "cruxeval-o": CRUXEvalOutput,
54
+ "cruxeval-i": CRUXEvalInput,
55
+ "cruxeval-o-cot": CRUXEvalOutputCoT,
56
+ "cruxeval-i-cot": CRUXEvalInputCoT,
47
57
  "gsm8k": GSM8K,
48
58
  "ifeval": IFEval,
49
59
  "ruler": RULER,
@@ -82,6 +92,10 @@ DESCRIPTIONS: dict[str, str] = {
82
92
  "humaneval-plus-tr": "HumanEval+ fully translated to Turkish, names included",
83
93
  "mbpp": "short Python functions from natural-language specifications",
84
94
  "mbpp-plus": "sanitized MBPP tasks with 39k+ EvalPlus test inputs",
95
+ "cruxeval-o": "predict what a short Python function returns for a given input",
96
+ "cruxeval-i": "find an input that makes a short Python function return a given output",
97
+ "cruxeval-o-cot": "CRUXEval output prediction with the step-by-step prompt",
98
+ "cruxeval-i-cot": "CRUXEval input prediction with the step-by-step prompt",
85
99
  "gsm8k": "multi-step grade-school math problems with exact numeric answers",
86
100
  "ifeval": "prompts with code-checkable instruction-following constraints",
87
101
  "ruler": "practical long-context retrieval across all 13 RULER task families",
@@ -0,0 +1,380 @@
1
+ """CRUXEval - predict what a short Python function returns, or what it was given.
2
+
3
+ 800 functions, each with one input and the output running it produces. The
4
+ output task (``cruxeval-o``) shows the input and asks for the output; the input
5
+ task (``cruxeval-i``) shows the output and asks for any input that produces it.
6
+ Both come in the upstream direct and chain-of-thought prompts, as separate
7
+ registry keys so the gap between them shows up as two jobs side by side.
8
+
9
+ Scoring needs no judge. A predicted output is parsed with ``ast.literal_eval``
10
+ and compared with ``==`` against the recorded output, so ``'abc'`` and
11
+ ``"abc"``, or ``(1, 2)`` written with odd spacing, all match. The one fallback:
12
+ when the recorded output is a string and the prediction is not a literal at
13
+ all, the bare text is compared with that string, so ``== abc`` counts for
14
+ ``'abc'``. Anything else that is not a literal - an unsimplified expression, a
15
+ call - is wrong, which is stricter than upstream (it evaluates expressions)
16
+ and keeps model-written code out of output scoring entirely.
17
+
18
+ A predicted input is never string-matched, because many inputs can produce
19
+ the same output: the call is run against the function in the sandbox and
20
+ passes when it returns the recorded output.
21
+ """
22
+
23
+ import ast
24
+ import json
25
+ import re
26
+ import secrets
27
+ from pathlib import Path
28
+
29
+ from benchkit.benchmarks.base import Task
30
+ from benchkit.benchmarks.utils import strip_think_tags
31
+ from benchkit.evaluation import EvaluationResult
32
+ from benchkit.executor import execute_with_feedback
33
+
34
+ DATASET = Path(__file__).parent.parent / "datasets" / "cruxeval.jsonl"
35
+
36
+ # Prompts are the upstream ones from facebookresearch/cruxeval (MIT), verbatim.
37
+ DIRECT_OUTPUT = """You are given a Python function and an assertion containing an input to the function. Complete the assertion with a literal (no unsimplified expressions, no function calls) containing the output when executing the provided code on the given input, even if the function is incorrect or incomplete. Do NOT output any extra information. Provide the full assertion with the correct output in [ANSWER] and [/ANSWER] tags, following the examples.
38
+
39
+ [PYTHON]
40
+ def f(n):
41
+ return n
42
+ assert f(17) == ??
43
+ [/PYTHON]
44
+ [ANSWER]
45
+ assert f(17) == 17
46
+ [/ANSWER]
47
+
48
+ [PYTHON]
49
+ def f(s):
50
+ return s + "a"
51
+ assert f("x9j") == ??
52
+ [/PYTHON]
53
+ [ANSWER]
54
+ assert f("x9j") == "x9ja"
55
+ [/ANSWER]
56
+
57
+ [PYTHON]
58
+ {code}
59
+ assert f({input}) == ??
60
+ [/PYTHON]
61
+ [ANSWER]
62
+ """
63
+
64
+ COT_OUTPUT = """You are given a Python function and an assertion containing an input to the function. Complete the assertion with a literal (no unsimplified expressions, no function calls) containing the output when executing the provided code on the given input, even if the function is incorrect or incomplete. Do NOT output any extra information. Execute the program step by step before arriving at an answer, and provide the full assertion with the correct output in [ANSWER] and [/ANSWER] tags, following the examples.
65
+
66
+ [PYTHON]
67
+ def f(s):
68
+ s = s + s
69
+ return "b" + s + "a"
70
+ assert f("hi") == ??
71
+ [/PYTHON]
72
+ [THOUGHT]
73
+ Let's execute the code step by step:
74
+
75
+ 1. The function f is defined, which takes a single argument s.
76
+ 2. The function is called with the argument "hi", so within the function, s is initially "hi".
77
+ 3. Inside the function, s is concatenated with itself, so s becomes "hihi".
78
+ 4. The function then returns a new string that starts with "b", followed by the value of s (which is now "hihi"), and ends with "a".
79
+ 5. The return value of the function is therefore "bhihia".
80
+ [/THOUGHT]
81
+ [ANSWER]
82
+ assert f("hi") == "bhihia"
83
+ [/ANSWER]
84
+
85
+ [PYTHON]
86
+ {code}
87
+ assert f({input}) == ??
88
+ [/PYTHON]
89
+ [THOUGHT]
90
+ """
91
+
92
+ DIRECT_INPUT = """You will be given a function f and an output in the form f(??) == output. Find any input such that executing f on the input leads to the given output. There may be multiple answers, but you should only output one. In [ANSWER] and [/ANSWER] tags, complete the assertion with one such input that will produce the output when executing the function.
93
+
94
+ [PYTHON]
95
+ def f(my_list):
96
+ count = 0
97
+ for i in my_list:
98
+ if len(i) % 2 == 0:
99
+ count += 1
100
+ return count
101
+ assert f(??) == 3
102
+ [/PYTHON]
103
+ [ANSWER]
104
+ assert f(["mq", "px", "zy"]) == 3
105
+ [/ANSWER]
106
+
107
+ [PYTHON]
108
+ def f(s1, s2):
109
+ return s1 + s2
110
+ assert f(??) == "banana"
111
+ [/PYTHON]
112
+ [ANSWER]
113
+ assert f("ba", "nana") == "banana"
114
+ [/ANSWER]
115
+
116
+ [PYTHON]
117
+ {code}
118
+ assert f(??) == {output}
119
+ [/PYTHON]
120
+ [ANSWER]
121
+ """
122
+
123
+ COT_INPUT = """You will be given a function f and an output in the form f(??) == output. Your task is to find any input such that executing f on the input leads to the given output. There may be multiple answers, but only output one. First, think step by step. You MUST surround the answer with [ANSWER] and [/ANSWER] tags. Express your answer as a passing assertion containing the input and the given output.
124
+
125
+ [PYTHON]
126
+ def f(x):
127
+ return x + 1
128
+ assert f(??) == 17
129
+ [/PYTHON]
130
+ [THOUGHT]
131
+ To find an input such that executing f on the input leads to the given output, we can work backwards from the given assertion. We know that f(??) == 17.
132
+
133
+ Since the function f(x) returns x + 1, for f(??) to be equal to 17, the value of ?? should be 16.
134
+ [/THOUGHT]
135
+ [ANSWER]
136
+ assert f(16) == 17
137
+ [/ANSWER]
138
+
139
+ [PYTHON]
140
+ {code}
141
+ assert f(??) == {output}
142
+ [/PYTHON]
143
+ [THOUGHT]
144
+ """
145
+
146
+ TEMPLATES = {
147
+ ("output", "direct"): DIRECT_OUTPUT,
148
+ ("output", "cot"): COT_OUTPUT,
149
+ ("input", "direct"): DIRECT_INPUT,
150
+ ("input", "cot"): COT_INPUT,
151
+ }
152
+
153
+ NO_ASSERTION = (
154
+ "No assertion of the form assert f(...) == ... was found in the answer. "
155
+ "Reply with one such assertion inside [ANSWER] and [/ANSWER] tags."
156
+ )
157
+
158
+
159
+ def _answer_block(response: str) -> str:
160
+ """The text the model meant as its answer.
161
+
162
+ The prompt itself opens ``[ANSWER]`` (direct) or ``[THOUGHT]`` (CoT), so a
163
+ response may carry both tags, only the closing one, or neither. The last
164
+ complete block wins; a Markdown fence inside it is unwrapped.
165
+ """
166
+ text = strip_think_tags(response)
167
+ if "[ANSWER]" in text:
168
+ text = text.rsplit("[ANSWER]", 1)[1].split("[/ANSWER]", 1)[0]
169
+ elif "[/ANSWER]" in text:
170
+ text = text.rsplit("[/ANSWER]", 1)[0]
171
+ text = text.rsplit("[/THOUGHT]", 1)[-1]
172
+ fences = re.findall(r"```[\w-]*\n?(.*?)```", text, re.DOTALL)
173
+ if fences:
174
+ text = fences[-1]
175
+ return text.strip()
176
+
177
+
178
+ def _assertion(block: str) -> str | None:
179
+ """The last ``assert`` statement in the block, if any."""
180
+ lines = block.splitlines()
181
+ for index in range(len(lines) - 1, -1, -1):
182
+ if lines[index].lstrip().startswith("assert"):
183
+ tail = "\n".join(line.strip() for line in lines[index:])
184
+ return tail if _parses(tail) else lines[index].strip()
185
+ return None
186
+
187
+
188
+ def _parses(source: str) -> bool:
189
+ try:
190
+ ast.parse(source)
191
+ except SyntaxError:
192
+ return False
193
+ return True
194
+
195
+
196
+ def _is_f_call(node: ast.expr) -> bool:
197
+ return (
198
+ isinstance(node, ast.Call)
199
+ and isinstance(node.func, ast.Name)
200
+ and node.func.id == "f"
201
+ )
202
+
203
+
204
+ def _equality(source: str) -> tuple[str, ast.expr, ast.expr] | None:
205
+ """Split ``[assert] left == right`` into its parsed sides."""
206
+ text = source.strip()
207
+ if text.startswith("assert"):
208
+ text = text[len("assert") :].strip()
209
+ try:
210
+ node = ast.parse(text, mode="eval").body
211
+ except SyntaxError:
212
+ return None
213
+ if (
214
+ isinstance(node, ast.Compare)
215
+ and len(node.ops) == 1
216
+ and isinstance(node.ops[0], ast.Eq)
217
+ ):
218
+ return text, node.left, node.comparators[0]
219
+ return None
220
+
221
+
222
+ def predicted_output(response: str) -> str | None:
223
+ """The output the model predicted, as source text."""
224
+ block = _answer_block(response)
225
+ candidate = _assertion(block) or block
226
+ split = _equality(candidate)
227
+ if split is not None:
228
+ text, left, right = split
229
+ side = left if _is_f_call(right) and not _is_f_call(left) else right
230
+ return ast.get_source_segment(text, side)
231
+ # Not parseable: a bare word after ``==`` is still a usable prediction.
232
+ if "==" in candidate:
233
+ candidate = candidate.rsplit("==", 1)[1]
234
+ candidate = candidate.split("#", 1)[0].strip()
235
+ return candidate or None
236
+
237
+
238
+ def predicted_call(response: str) -> str | None:
239
+ """The ``f(...)`` call the model proposed, as source text."""
240
+ block = _answer_block(response)
241
+ candidate = _assertion(block) or block
242
+ split = _equality(candidate)
243
+ if split is not None:
244
+ text, left, right = split
245
+ for side in (left, right):
246
+ if _is_f_call(side):
247
+ return ast.get_source_segment(text, side)
248
+ return None
249
+ try:
250
+ node = ast.parse(candidate, mode="eval").body
251
+ except SyntaxError:
252
+ return None
253
+ if _is_f_call(node):
254
+ return ast.get_source_segment(candidate, node)
255
+ # A bare input, such as ``"ba", "nana"``, is the argument list itself.
256
+ return f"f({candidate})"
257
+
258
+
259
+ def outputs_match(predicted: str | None, expected: str) -> bool:
260
+ """Literal comparison of a predicted output with the recorded one."""
261
+ if predicted is None:
262
+ return False
263
+ target = ast.literal_eval(expected)
264
+ try:
265
+ value = ast.literal_eval(predicted)
266
+ except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
267
+ return isinstance(target, str) and predicted.strip() == target
268
+ try:
269
+ return bool(value == target)
270
+ except Exception:
271
+ return False
272
+
273
+
274
+ class _CRUXEval:
275
+ name: str
276
+ task: str # "output" or "input"
277
+ style: str # "direct" or "cot"
278
+ task_count = 800
279
+
280
+ def load_tasks(self) -> list[Task]:
281
+ tasks = []
282
+ with open(DATASET, encoding="utf-8") as f:
283
+ for line in f:
284
+ d = json.loads(line)
285
+ tasks.append(
286
+ Task(
287
+ id=d["task_id"],
288
+ prompt=d["function"],
289
+ metadata={
290
+ "input": d["input"],
291
+ "output": d["output"],
292
+ # The demo client answers with this.
293
+ "canonical_solution": (
294
+ f"[ANSWER]\nassert f({d['input']}) == "
295
+ f"{d['output']}\n[/ANSWER]"
296
+ ),
297
+ },
298
+ )
299
+ )
300
+ return tasks
301
+
302
+ def build_prompt(self, task: Task) -> str:
303
+ template = TEMPLATES[(self.task, self.style)]
304
+ return template.format(
305
+ code=task.prompt,
306
+ input=task.metadata["input"],
307
+ output=task.metadata["output"],
308
+ )
309
+
310
+ def evaluate(self, task: Task, response: str) -> bool:
311
+ return self.evaluate_with_feedback(task, response).passed
312
+
313
+ def summary_fields(self, records: list[object]) -> dict:
314
+ """Record which half and which prompt style a score came from."""
315
+ return {"cruxeval_task": self.task, "cruxeval_prompt": self.style}
316
+
317
+
318
+ class _OutputPrediction(_CRUXEval):
319
+ task = "output"
320
+
321
+ def evaluate_with_feedback(self, task: Task, response: str) -> EvaluationResult:
322
+ predicted = predicted_output(response)
323
+ if predicted is None:
324
+ return EvaluationResult(0.0, NO_ASSERTION)
325
+ if outputs_match(predicted, task.metadata["output"]):
326
+ return EvaluationResult(1.0)
327
+ return EvaluationResult(
328
+ 0.0,
329
+ "The predicted output does not equal the value the function returns. "
330
+ "Trace the code again; the answer must be a Python literal.",
331
+ )
332
+
333
+
334
+ class _InputPrediction(_CRUXEval):
335
+ task = "input"
336
+ evaluation_activity = "running the predicted input"
337
+
338
+ def evaluate_with_feedback(self, task: Task, response: str) -> EvaluationResult:
339
+ call = predicted_call(response)
340
+ if call is None:
341
+ return EvaluationResult(0.0, NO_ASSERTION)
342
+ # Same check as upstream, ``assert output == f(input)``. The input is
343
+ # model-written code, so a pass also needs a fresh sentinel printed
344
+ # after the assertion: an input that exits early cannot fake one.
345
+ sentinel = secrets.token_hex(16)
346
+ program = (
347
+ f"{task.prompt}\n\n"
348
+ f"assert {task.metadata['output']} == {call}\n"
349
+ f"print({sentinel!r})\n"
350
+ )
351
+ result = execute_with_feedback(program, timeout=5, sentinel=sentinel)
352
+ if result.passed:
353
+ return EvaluationResult(1.0)
354
+ if "assertion failed" in result.feedback:
355
+ return EvaluationResult(
356
+ 0.0,
357
+ "The proposed input ran, but the function returned a different "
358
+ "value than the given output.",
359
+ )
360
+ return EvaluationResult(0.0, result.feedback)
361
+
362
+
363
+ class CRUXEvalOutput(_OutputPrediction):
364
+ name = "cruxeval-o"
365
+ style = "direct"
366
+
367
+
368
+ class CRUXEvalOutputCoT(_OutputPrediction):
369
+ name = "cruxeval-o-cot"
370
+ style = "cot"
371
+
372
+
373
+ class CRUXEvalInput(_InputPrediction):
374
+ name = "cruxeval-i"
375
+ style = "direct"
376
+
377
+
378
+ class CRUXEvalInputCoT(_InputPrediction):
379
+ name = "cruxeval-i-cot"
380
+ style = "cot"
@@ -39,8 +39,10 @@ _RELEASE_FILE = "dataset.json"
39
39
  # files tasks.jsonl names are fetched (not the miner that built it), and every
40
40
  # task pins its source archive and hidden tests by sha256, which load_tasks
41
41
  # verifies, so a bad or moved upstream file fails loudly instead of scoring.
42
+ # The revision is a pinned commit: a cache fetched from any other revision is
43
+ # fetched again, so an errata release never grades with stale hidden tests.
42
44
  _HF_DATASET = "DogukanUrker/PatchEval"
43
- _HF_REVISION = "main"
45
+ _HF_REVISION = "13c6964b497f6f118bf5af51a2b1b39cc2d612e6"
44
46
  _RELEASE = "pilot-20"
45
47
  _COMPLETE = ".complete"
46
48
  _FETCH_LOCK = threading.Lock()
@@ -178,11 +180,19 @@ def _dataset_dir() -> Path:
178
180
  return _configured_root() or _cache_dir()
179
181
 
180
182
 
183
+ def _cached_revision_matches(root: Path) -> bool:
184
+ try:
185
+ marker = (root / _COMPLETE).read_text(encoding="utf-8")
186
+ except OSError:
187
+ return False
188
+ return marker.strip() == _HF_REVISION
189
+
190
+
181
191
  def _dataset_installed() -> bool:
182
192
  configured = _configured_root()
183
193
  if configured is not None:
184
194
  return (configured / _RELEASE_FILE).is_file()
185
- return (_cache_dir() / _COMPLETE).is_file()
195
+ return _cached_revision_matches(_cache_dir())
186
196
 
187
197
 
188
198
  def _fetch_release(progress: Progress | None = None) -> Path:
@@ -192,8 +202,9 @@ def _fetch_release(progress: Progress | None = None) -> Path:
192
202
  return configured
193
203
  root = _cache_dir()
194
204
  with _FETCH_LOCK:
195
- if (root / _COMPLETE).is_file():
205
+ if _cached_revision_matches(root):
196
206
  return root
207
+ (root / _COMPLETE).unlink(missing_ok=True)
197
208
  root.mkdir(parents=True, exist_ok=True)
198
209
  done = 0
199
210
  with vision._http_client() as client:
@@ -724,6 +735,7 @@ class PatchEval:
724
735
  def __init__(self, dataset_root: Path | None = None) -> None:
725
736
  self.dataset_root = dataset_root
726
737
  self.release = "unloaded"
738
+ self.revision: int | None = None
727
739
 
728
740
  def _root(self) -> Path:
729
741
  return (self.dataset_root or _fetch_release()).resolve()
@@ -753,6 +765,8 @@ class PatchEval:
753
765
  f"PatchEval dataset must use schema_version {_SCHEMA_VERSION}"
754
766
  )
755
767
  self.release = str(release.get("release") or "unknown")
768
+ revision = release.get("revision")
769
+ self.revision = revision if isinstance(revision, int) else None
756
770
  try:
757
771
  records = [
758
772
  json.loads(line)
@@ -774,7 +788,10 @@ class PatchEval:
774
788
  return [Task(spec.id, "", {"spec": spec}) for spec in specs]
775
789
 
776
790
  def result_metadata(self, _variant: str | None) -> dict:
777
- return {"dataset_release": self.release}
791
+ metadata: dict = {"dataset_release": self.release}
792
+ if self.revision is not None:
793
+ metadata["dataset_revision"] = self.revision
794
+ return metadata
778
795
 
779
796
  def build_prompt(self, task: Task) -> str:
780
797
  spec = self._spec(task)
@@ -9,6 +9,7 @@ import string
9
9
  import urllib.request
10
10
  import uuid
11
11
  from collections.abc import Callable
12
+ from functools import lru_cache
12
13
  from pathlib import Path
13
14
 
14
15
  from benchkit.benchmarks.base import Task
@@ -45,6 +46,14 @@ TASKS_PER_BUCKET = DEFAULT_SAMPLES_PER_KIND * len(TASK_KINDS)
45
46
  OUTPUT_RESERVE_TOKENS = 256
46
47
  FALLBACK_CHARS_PER_TOKEN = 4
47
48
  FIT_TOLERANCE = 0.01
49
+ # cwe and fwe share a 2,000-word vocabulary. cwe draws one new uncommon word
50
+ # per unit (~40 chars), so its vocabulary grows with the bucket; 2,000 words
51
+ # stop at about 18k tokens.
52
+ VOCABULARY_SIZE = 2000
53
+ # Every renderer has a floor below which extra units change nothing (cwe and
54
+ # qa at one, fwe at ten, qa_2's ten gold documents), so the fallback sizes a
55
+ # unit between two counts above all of them.
56
+ _PROBE_UNITS = (16, 80)
48
57
 
49
58
  _VARIABLE_NOISE = (
50
59
  "The grass is green. The sky is blue. The sun is yellow. "
@@ -195,8 +204,9 @@ def _aggregation_task(bucket: int, sample: int, kind: str) -> Task:
195
204
  }
196
205
  if kind in {"cwe", "fwe"}:
197
206
  rng = random.Random(seed)
207
+ size = max(VOCABULARY_SIZE, bucket // 8) if kind == "cwe" else VOCABULARY_SIZE
198
208
  vocabulary = [
199
- "".join(rng.choices(string.ascii_lowercase, k=6)) for _ in range(2000)
209
+ "".join(rng.choices(string.ascii_lowercase, k=6)) for _ in range(size)
200
210
  ]
201
211
  metadata["vocabulary"] = list(dict.fromkeys(vocabulary))
202
212
  metadata["answers"] = metadata["vocabulary"][: 10 if kind == "cwe" else 3]
@@ -266,6 +276,7 @@ def _essay_words() -> list[str]:
266
276
  return text.split()
267
277
 
268
278
 
279
+ @lru_cache(maxsize=len(QA_FILES))
269
280
  def _qa_records(kind: str) -> tuple[list[dict], list[str]]:
270
281
  raw = json.loads(_qa_path(kind).read_text(encoding="utf-8"))
271
282
  if kind == "qa_1":
@@ -410,7 +421,19 @@ def _render_niah(task: Task, filler_units: int) -> str:
410
421
  target_values=list(metadata["answers"]),
411
422
  distractor_values=[p[1] for p in pairs],
412
423
  )
413
- context = "\n".join(_insert_at_positions(filler, needles, seed ^ 0xA51CE))
424
+ lines = _insert_at_positions(filler, needles, seed ^ 0xA51CE)
425
+ if metadata["haystack"] == "essay":
426
+ # Running prose, not one word per line: a newline per word costs far
427
+ # more tokens than the character estimate assumes.
428
+ needle_set = set(needles)
429
+ chunks: list[list[str]] = [[]]
430
+ for line in lines:
431
+ if line in needle_set:
432
+ chunks.extend([[line], []])
433
+ else:
434
+ chunks[-1].append(line)
435
+ lines = [" ".join(chunk) for chunk in chunks if chunk]
436
+ context = "\n".join(lines)
414
437
  return (
415
438
  "Some special magic numbers are hidden within the following text. "
416
439
  "Memorize the records because you will be asked for one key afterward.\n\n"
@@ -463,11 +486,17 @@ def render_prompt(task: Task, filler_units: int) -> str:
463
486
 
464
487
 
465
488
  def _fallback_units(task: Task, target_tokens: int) -> int:
466
- base = render_prompt(task, 0)
467
- one = render_prompt(task, 1)
468
- chars_per_unit = max(1, len(one) - len(base))
489
+ low, high = _PROBE_UNITS
490
+ low_chars = len(render_prompt(task, low))
491
+ chars_per_unit = max(
492
+ 1.0, (len(render_prompt(task, high)) - low_chars) / (high - low)
493
+ )
469
494
  target_chars = target_tokens * FALLBACK_CHARS_PER_TOKEN
470
- return max(0, (target_chars - len(base)) // chars_per_unit)
495
+ return max(0, low + math.floor((target_chars - low_chars) / chars_per_unit))
496
+
497
+
498
+ def _estimate_tokens(prompt: str) -> int:
499
+ return math.ceil(len(prompt) / FALLBACK_CHARS_PER_TOKEN)
471
500
 
472
501
 
473
502
  def fit_prompt(
@@ -479,9 +508,12 @@ def fit_prompt(
479
508
  """Fit a generated prompt just below its context bucket.
480
509
 
481
510
  When the server exposes a tokenizer, a short secant-style calibration keeps
482
- the prompt within one percent of the target. Generic servers fall back to a
483
- conservative character-to-token estimate.
511
+ the prompt within one percent of the target. Generic servers run the same
512
+ calibration against a conservative character-to-token estimate, and the
513
+ measured count comes back as ``None``.
484
514
  """
515
+ estimated = token_count is None
516
+ count = _estimate_tokens if token_count is None else token_count
485
517
  bucket = int(task.metadata["context_tokens"])
486
518
  target = max(1, bucket - OUTPUT_RESERVE_TOKENS)
487
519
  units = (
@@ -490,10 +522,7 @@ def fit_prompt(
490
522
  else _fallback_units(task, target)
491
523
  )
492
524
  prompt = render_prompt(task, units)
493
- if token_count is None:
494
- return prompt, units, None
495
-
496
- measured = token_count(prompt)
525
+ measured = count(prompt)
497
526
  if not isinstance(measured, int) or measured <= 0:
498
527
  return prompt, units, None
499
528
 
@@ -502,7 +531,7 @@ def fit_prompt(
502
531
  if measured <= target and measured >= target * (1 - FIT_TOLERANCE):
503
532
  break
504
533
  if base_tokens is None:
505
- base_tokens = token_count(render_prompt(task, 0))
534
+ base_tokens = count(render_prompt(task, 0))
506
535
  if not isinstance(base_tokens, int) or base_tokens < 0:
507
536
  base_tokens = 0
508
537
 
@@ -513,23 +542,24 @@ def fit_prompt(
513
542
  next_units = max(0, units - 1) if measured > target else units + 1
514
543
  units = next_units
515
544
  prompt = render_prompt(task, units)
516
- next_measured = token_count(prompt)
545
+ next_measured = count(prompt)
517
546
  if not isinstance(next_measured, int) or next_measured <= 0:
518
547
  return prompt, units, None
519
548
  measured = next_measured
520
549
 
521
550
  # Never knowingly send a prompt above the bucket. The linear adjustment is
522
551
  # normally exact; this final correction handles unusual tokenizer merges.
523
- for _ in range(2):
552
+ # Without a tokenizer it is cheap, so keep going until the estimate fits.
553
+ for _ in range(16 if estimated else 2):
524
554
  if measured <= target or units == 0:
525
555
  break
526
556
  excess = measured - target
527
557
  tokens_per_unit = max(1.0, measured / max(units, 1))
528
558
  units = max(0, units - math.ceil(excess / tokens_per_unit) - 1)
529
559
  prompt = render_prompt(task, units)
530
- next_measured = token_count(prompt)
560
+ next_measured = count(prompt)
531
561
  if not isinstance(next_measured, int) or next_measured <= 0:
532
562
  return prompt, units, None
533
563
  measured = next_measured
534
564
 
535
- return prompt, units, measured
565
+ return prompt, units, None if estimated else measured