Repository navigation
Record whether each prediction row's responses were live calls or cache replays - #231
Merged
Merged
Conversation
Prediction rows now carry live_calls, cache_hit_calls and live_elapsed_seconds, filled by every row writer (sync household and single-output, chunked, batch, row repair) from the same cache_hit flag the spend ledger records. Rows written before the columns existed stay empty, and the supervisor still accepts their scenario files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…is.py Hypothesis drives run_no_tools_eval and the single-output writer through the real request path (chunks, repair rounds, budget escalation and explanation repair) with a fake clock and per-call cache flags. It checks each row's invariants, the counts against the spend ledger's records of the same calls, the CSV, and a byte-identical run-store round trip. Further tests cover batch rows, row repair and its header-widening append, the retry merge, resumed outputs, run-store CSVs that predate the columns, and supervised resumes of scenario files written before them. household_call_provenance moves to policybench/call_provenance.py, so analysis.py, whose hash the paper prints as the scoring code, is unchanged. The runbook documents the columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…both APIs A row-repair merge that applies a repair file written before the provenance columns existed kept the source row's values, which described the replaced request and could exceed the repair's elapsed time. The merge now empties all three on such rows. The end-to-end property now also raises (non-fatal failures on answer and explanation requests), answers the requested tool (submit_explanations for explanation repairs), and runs the Responses API (GPT-5.4, and GPT-5.5 at three outputs per request). Its oracle is the mock's call history: a group with no failed request must count every returned response exactly, and a group with a failure must equal some subset of them, counts and times together. Shrinking is off for it, since each example runs a whole evaluation. Deterministic tests pin a replayed and a live explanation repair and a Responses API replay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Taking this PR over from session local_1bc636d9, which has been idle since 2026-10-14; I've messaged it. Review r2 (Sol) approved at 182decf with two non-blocking test gaps. I'll close them, merge main and get a fresh review. I'll merge it on green CI and an agreeing independent review, then remove its worktree. — session local_a8447b8d ("Finish five stale PolicyBench draft PRs") |
Review r2's two non-blocking gaps: - The subset oracle returned True above 200,000 combinations, so a row reporting 20 calls but 19 seconds from 28 one-second responses could pass. It is now an exact meet-in-the-middle search over the distinct durations (equal durations are interchangeable), checked against the combinations definition by a differential property; an example still too large is rejected, never passed. - The run-store round trip now asserts the typed live_elapsed_seconds in usage_json against the repo's own CSV reader, with non-null examples, and the end-to-end property checks it too. Five mutants (typed live time dropped from usage_json; the oracle ignoring multiplicity, the replay count or the live sum; a cap that passes large inputs) are all killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The single-output writer stores one response per output, so a scenario can hold a response with no live time next to one with a live time. The end-to-end check now asks that the stored value be one of the scenario's row values, or absent when one of them is empty. Found at the merged head (single_output, gpt-5.5, two outputs, one failing). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onses - The subset search's windows are twice the tolerance, and each candidate is then checked with the same pytest.approx comparisons as the brute-force definition. A 2.000001-second response no longer matches a row's 2.0, and sums whose window bound rounds past them are kept; both are pinned. - The differential property discards inputs where some choice's exact sum is within rounding of a tolerance's edge, where summation order decides. - The run-store check joins each prediction to its own response through source_attempt and compares the typed fields exactly with that output's CSV row, instead of with any row of the scenario. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2.000001 - 2.0 is 1.000000000139778e-6 in binary, and in the pinned one-tolerance window cases the lower bound for the second duration rounds above it, not below. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 25, 2026 08:47
Contributor
Author
|
Merged as d507d2b (squash, Gates at
No published number changes: the PR adds the row fields only. Evidence: |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to the latency replay audit (#228). That audit found 328 households in the frozen US run that are full disk-cache replays and 33 that are partial replays, and that their rows record the cache lookup as
elapsed_seconds. The spend ledger already recordscache_hitper call, but prediction rows did not, so analysis could not tell a replay from a call. Now every prediction row records where its responses came from.The fields
live_callscache_hit_callslive_elapsed_secondselapsed_secondsThe counts cover the request group whose time the row's
elapsed_secondsapportions. Forrun_no_tools_evalthat is the whole household: every chunk, repair round, budget escalation and explanation repair. For single-output runs and row repairs it is the one output, and for batch rows the one request. Every row of a group repeats the counts.live_elapsed_secondsis divided among the rows likeelapsed_seconds. All three come from_response_cache_hit, the flag the spend ledger already used. LiteLLM sets_hidden_params["cache_hit"] = Trueon every disk-cache replay (litellm/caching/caching_handler.py, read for this PR).Every writer
eval_no_tools):_request_predictions_onceand_request_explanations_oncerecord each answered request._aggregate_request_resultsand the chunked branch ofrun_single_no_toolssum them. Both row builders write them: household rows divide the live time bybatch_size, and single-output rows write it unchanged. Their exception rows record zero calls. Unknown in means unknown out: a result without provenance makes the aggregate empty rather than undercounted.batch_eval.rows_from_unit):cache_hit_callsis 0, because batch requests never read the disk cache.live_callsis 1 when a response answered the row, and the live time is empty, likeelapsed_seconds._append_rowsused to append under whatever header the file had. It now widens an older header first and keeps every existing cell's text, so a repair round resumed across this change cannot shift cells. It also replaces a headerless empty file, which it previously appended to without a header. A merge that applies an older repair, written without the columns, empties them on the row it replaces: the source's values described the replaced request and could otherwise exceed the repair's time.retry_eval) and the merges (chunked_eval,fold_board,supervisor.combine) concatenate frames, so the columns pass through and older rows stay empty. Tests cover the retry and row-repair merges._RESPONSE_COLUMNS). Their exact text is stashed for a byte-identical export, and typed copies go inresponses.usage_jsonfor SQL queries. A CSV written before the columns existed exports without them, byte for byte._scenario_completestill accepts a scenario file that predates the columns, so a supervised run resumed across this change does not refuse its finished scenarios. A file with only some of the three columns is rejected.call_provenance.household_call_provenanceclassifies each household aslive,partial_replay,full_replay,no_responseorunknown, with its summed live time. It is a new module, so this PR doesn't changeanalysis.py, whose hash the paper prints as the scoring code. Against the branch's base the file differs only by main's Give a model with no recorded tokens no cost, not $0 #230 cost change, which the merge brings in.Nothing published changes
latencySeconds,model_cost_latencyandusage_summary_by_modelare unchanged, and nothing published reads the new fields. A property test checks that adding the columns leaves both functions' output identical. Whether replays leave the median is decision d1542, which is still open, so it is not implemented here. The frozen board predates the fields, so all of its households classify asunknown, and a test reads the snapshot header to confirm it.Invariants
These hold for every row a writer emits.
tests/test_call_provenance.pyexecutes each one:live_callsandcache_hit_callsare non-negative integers. Their sum is every response behind the row's time, live or replayed.0 <= live_elapsed_seconds <= elapsed_seconds, and the two are empty together.live_calls == 0andlive_elapsed_seconds == 0exactly.live_elapsed_seconds == elapsed_secondsexactly. Both sums run over the same sequence, and float addition and division are monotone, so 2 to 4 hold exactly, not within a tolerance.pytest.approxcomparisons as the brute-forcecombinationsdefinition. A differential property checks the two agree. It discards inputs where some choice's exact sum is within rounding of a tolerance's edge, because there the order the seconds are added in can decide the comparison, and the two searches add them in different orders; tests pin the edge itself. An example too large for the search is discarded withhypothesis.reject(), never passed; none were in the runs below.model_cost_latencyandusage_summary_by_modelidentical.Intended non-invariant: the counts are not additive across rows (each row of a group repeats them).
live_elapsed_secondsis additive.How the end-to-end property runs
test_rows_record_each_responses_provenance_and_agree_with_the_ledgerdrivesrun_no_tools_evalandrun_no_tools_single_output_evalthrough the real request path:_request_predictions_once, escalation, repair rounds, explanation repair, retries, partial rounds, chunking, and failed chunks and households. Onlylitellm.completion,litellm.responsesand the clock are mocked. The models cover chat completions with one output per request (Claude Sonnet 4.6) and unchunked (Claude Opus 4.6), and the Responses API unchunked (GPT-5.4) and three outputs per request (GPT-5.5). Hypothesis draws each response's cache flag, duration and shape: an answer to the requested tool (submit_outputsorsubmit_explanations), values only, nothing, a length cut, or a failure. Over 100 examples it produced partial replays (53%), full replays (35%), live households (21%) and households with no response (2%). A request failed in 43% of examples, 43% used the Responses API, and 13% answered an explanation repair. Each example checks invariants 1 to 5 and 7 on the rows, the written CSV and a run-store round trip. In the store, each prediction is joined to its own response (source_attempt=attempt), and that response's typedlive_calls,cache_hit_callsandlive_elapsed_secondsinusage_jsonmust equal that output's CSV row exactly, as the store's reader parses it; an empty live time must be left out.test_responses_api_requests_record_replays_and_live_callspins a Responses API replay followed by a live repair.Mutation check
Twenty-two mutants, applied one at a time, each break one rule: a replay keeping its time or counting as live, the aggregate counting only one response or ignoring an unknown one, the chunk path dropping provenance, a failed chunk turning provenance unknown, live time not being split, explanation repairs losing provenance or hiding a replay, answer replays going unseen, a failed household turning unknown, batch rows counting a hit, repair rows dropping counts,
_append_rowsignoring the header, the legacy-repair merge keeping the source's values, the run store skipping the typed copies, the supervisor requiring the columns or accepting some of them, and the classifier misreading mixes and unknowns. All 22 are killed (~/reviews/policybench-row-cache-hit-calls/mutants-r2.log). Two first survived and led to tests: the supervisor requiring the columns (test_scenario_written_before_call_provenance_is_still_complete) and explanation replays going unseen (test_an_explanation_repair_records_its_own_provenance, which kills it on its own).Review r2's follow-ups added five more mutants, all killed (
~/reviews/policybench-row-cache-hit-calls/r3/mutants-r3.log):live_elapsed_secondsdropped from the run store's typed fields, and the subset oracle ignoring multiplicity, the replay count or the live sum, or passing large inputs again. The live-sum mutant first survived; the differential property then gained targets with the total right but the live time wrong.Review r3's follow-ups added six more, all killed (
~/reviews/policybench-row-cache-hit-calls/r4/mutants-r4.log):r4/swap-mutant-old-vs-new.txt);live_elapsed_secondsdropped from the typed fields again.The one-tolerance mutant first survived. A search (
r4/window_search.py) then found sums it drops: the lower bound for the second of two durations, computed as the window's lower edge less the first, rounds above it, though the two still add up to within tolerance. Two such cases are now pinned.Review
Round 1 (GPT-6.1 Sol at ce67c35) asked for changes: an older repair file kept the source row's provenance (blocking, fixed above); invariant 5 was overstated and failures were untested; successful explanation repairs and the Responses API were uncovered. All three are addressed at 182decf.
Round 2 (GPT-6.1 Sol at 182decf) approved, with two non-blocking test gaps, both closed at 4c323c4:
Trueabove 200,000 combinations, so 28 one-second live responses kept as "20 calls, 19 seconds" could pass. It is now the subset search described under invariant 5, andtest_the_subset_search_checks_what_combinations_could_notpins that case.live_elapsed_seconds.test_run_store_round_trips_provenance_byte_for_bytenow asserts it against the repo's own CSV reader (pandas' default parser, which can land an ulp from the written float) and closely against the drawn value, with explicit non-null examples.Main is merged in (f21b292). At the merged head the new end-to-end assertion found its own over-strict expectation: the single-output writer stores one response per output, so a scenario can hold a response with no live time next to one with a live time. cdf7975 then let each stored value match any of the scenario's rows, as the count checks did. That was the test, not the writers.
Round 3 (GPT-6.1 Sol at cdf7975) approved, with three non-blocking follow-ups, all closed at d8abc3f:
pytest.approx. A 2.000001-second live response is 1.000000000139778e-6 from a row's 2.0 in binary, past the 1e-6 tolerance, but lay inside the window as floats computed it. The search now checks each candidate withpytest.approx, and the PR no longer calls it exact (invariant 5).test_the_subset_search_decides_the_tolerance_edge_as_approx_doespins both sides of the edge for the live time and the total.analysis.pysentence names the comparison base.Round 4 (GPT-6.1 Sol at d8abc3f) approved, with three non-blocking wording corrections, all made in
b6d13931(a comment and a docstring intests/test_call_provenance.py, and this body):2.000001 - 2.0is1.000000000139778e-6, not1.0000000000139778e-6.7.6204078397967105 > 7.62040783979671,482.7581355537797 > 482.75813555377965), not below.Tests
PYTHONHASHSEED=0 pytest -m "not slow" -n 4at 182decf: 5169 passed, 58 skipped. Without the pinned seed, xdist workers collecttests/test_reference_audit_pins.pyin different orders, because it parametrizes over frozensets; that predates this PR, CI runs without-n, and a separate task fixes it.~/reviews/policybench-row-cache-hit-calls/r3/), the slow payload checks give 4 passed, 6 skipped (slow-payload-merged.log). That includestest_the_committed_payload_rebuilds_through_export_and_the_carry; the skips need release stages that aren't on this machine. d8abc3f changes onlytests/test_call_provenance.py, which they don't use.~/reviews/policybench-row-cache-hit-calls/r4/):tests/test_call_provenance.pypass under each of Hypothesis seeds 1–8 (seeds.log);PYTHONHASHSEED=0 pytest -n 2 -m "not slow"over the provenance, tool-pilot, carried-usage, cost-latency, run-store, eval, row-repair, retry, batch, sweep-timing and supervisor test files gives 664 passed, 5 skipped (related.log).d8abc3f9. Lint, app and paper pass. The test job's first attempt stalled at 88%, insidetests/test_runstore.pyandtests/test_scenarios.py, and hit its 30-minute timeout; no test failed. Those files are unchanged here and pass locally in 24 seconds. On the rerun,pytest -m "not slow"gave 5480 passed, 79 skipped, 36 deselected: the 5,559 tests a local collection finds, 19 more than atcdf7975e.b6d13931, which changes only a comment and a docstring after round 4: the 44 tests intests/test_call_provenance.pypass (r4/provenance-after-wording.log). CI run 40176448694: lint, app, paper and test pass, andpytest -m "not slow"gives 5507 passed, 79 skipped, 36 deselected. The run tests the branch merged with current main, which now includes Disclose why 330 frozen households record near-zero latency and two models record none #228.ruff format --checkandruff checkare clean on the changed files.axiom: n/a: benchmark harness instrumentation, no policy change.
🤖 Generated with Claude Code