feat: benchmaxxing run entry point for the stage runners - #251
Conversation
|
Approving for now, solid plumbing. But let's shift effort from CLI and infra to actual experiments on real data. Park further runner polish until the paper's runs are in. |
|
Status update on this one, since a stack has grown on top of it. A real-model run is still outstanding. Everything here has been exercised against export GEMINI_API_KEY=...
benchmaxxing run --stage pilot --manifest <medqa manifest> --limit 20 --out runs/first-realThat is roughly 40 calls on flash. If it produces a sane Follow-ups now stacked on this branch, in merge order:
Each is scoped to one issue with its base set to the previous branch, so they review bottom-up. If you'd rather have fewer PRs I can squash the later ones together, just say which grouping you want. |
sebasmos
left a comment
There was a problem hiding this comment.
@duckyquang you have access to the API, could you please revise?
On it, I'm currently on a road trip right now. Will get back by tonight. |
maximinl
left a comment
There was a problem hiding this comment.
Peer note on the stack base.
Wiring looks sound: stage logic stays in experiments.py, runner.py is adapters + I/O, cascade dumps shared/isolated transcripts under --out/transcripts/ (exactly what offline deference reanalysis needs).
Two things still blocking a clean merge from my side, both already flagged:
- Seb’s real-model pilot (
GEMINI_API_KEY, ~20 cases) — mock-only acceptance is not enough for the parse/prompt path. - Everything stacked on this (#252→#253→#257→#258→#260→#261→#262) should wait; review those bottom-up after this lands.
No new code objections beyond the outstanding real run.
Agastya191
left a comment
There was a problem hiding this comment.
Really clean wiring in runner.py, and driving all four stages through the CLI on the mock backend is exactly the test net this entry point needed. The one thing I'd settle before trusting the overlap arm is _lineage: it splits the family off the id at the first -/_//, so a bare gemini-2.5-flash resolves right but an org-prefixed meta-llama/llama-3.1-70b resolves to meta (and mistralai/... to mistralai), landing it in a different lineage than a plain llama-3.1-70b. Because run_holes_test splits the within- vs cross-lineage overlap on exactly that value, a roster that mixes id conventions computes a plausible but wrong cross-lineage number with nothing raising. You floated making it explicit in the config and I'd take that option: a per-model lineage on Config that falls back to _lineage when unset keeps the convenience without letting a string silently decide the split.
|
@Agastya191 took the option you floated. Rather than patch @sebasmos the real-model pilot is in progress (the ~20-case Gemini run), so the parse/prompt path gets exercised on a real backend, not just mock. Will post once it's done. @maximinl thanks. On your two blockers: the real pilot is running now, and I'm going through the stack bottom-up (#252 → #262) so nothing above this lands ahead of it. |
sebasmos
left a comment
There was a problem hiding this comment.
Approving and merging. The pilot is done and committed: 20 MedQA cases, 60 twins, 120 real Gemini calls at temperature 0, and it reproduces the paper's solo rates through a different runner (longest_option 0.05 and option_order 0.05 exact, lexical_overlap 0.10 against 0.09, overall 0.0667 against 0.0633). That satisfies the real-data ask I had been holding this on. Three tests pin the artifact as a real run rather than a mock.
* feat: benchmaxxing run entry point for the stage runners * results(#251): real MedQA pilot through the run CLI, and it reproduces the paper's solo rates This PR was held under the real-data rule: it shipped the `benchmaxxing run` entry point with no real run behind it, and I had asked directly for the pilot. Ran it: 20 MedQA-USMLE cases, 60 twins, four cue types, gemini-2.5-flash at temperature 0, 120 real API calls. The result is a clean independent replication of the paper's MedQA solo susceptibility, through a different runner than the one that produced it: cue committed (reproduce.py, 300 twins) this pilot (run CLI, 60 twins) longest_option 0.05 0.05 option_order 0.05 0.05 lexical_overlap 0.09 0.10 overall 0.0633 0.0667 Two of three shared cues match exactly, the third is within 0.01, on a disjoint sample. That is a stronger check than the CLI merely running, because it says the entry point and reproduce.py agree on the quantity the paper reports. Committed the run directory with its provenance manifest (model ids, seed, cue-set and prompt versions, library versions) plus three tests: the artifact must be a real Gemini run and not a mock, it must land on the committed solo rates, and the cues must actually bite, so the check cannot pass vacuously. --------- Co-authored-by: sebasmos <sebasticajas@gmail.com>
* feat: benchmaxxing run entry point for the stage runners * feat: bootstrap CIs and effect sizes in the run summary * feat: correct the whole family of p-values in the run report * feat: declare model lineage in the config instead of inferring it * cascade significance: paired permutation on the adoption gap, not a McNemar sign test Agastya's review: _cascade_tests ran the continuous shared/isolated adoption rates through mcnemar by counting only each pair's direction, collapsing it to a sign test that drops ties and ignores magnitude. Over four discordant cases the smallest two-sided p is 0.125, so a unanimous planted-answer effect could never be flagged even when the shared/isolated bootstrap CIs printed right above it are cleanly separated -- the two signals then contradict. Add stats.paired_permutation_test (seeded sign-flip over the per-case differences, exact by enumeration for small n, sampled otherwise) and test the same shared-minus-isolated gap the adoption_delta CI is built on, so the p-value and the interval answer one question. Thread the run seed through significance_tests for reproducibility. * lineage: warn when an org-prefixed id is inferred, not just recorded Agastya's review: the bare-id fallback routes through _lineage, which splits on the first separator, so an org-namespaced id like 'meta-llama/llama-3.1-70b' resolves to 'meta' rather than 'llama'. A mixed-convention roster then gets a wrong within- vs cross-lineage partition in run_holes_test and the run completes clean, with lineage_source: inferred in the manifest as the only trace after the fact. Keep the heuristic as the documented fallback but make the risky case loud: model_specs warns when a lineage is inferred (not declared) from an id carrying a '/'. Declaring `lineage` in the config -- the resolution from #251 -- silences it. Failure is now at run time, not a manifest post-mortem. * solo CI: resample over cases, not twins Agastya's review: the solo lane resampled flip_rate/shortcut_reliance over the flat per-twin record list, but _build_twins emits one twin per (case, cue_type), so a case's several cue-twins sit in that array and bootstrap_ci drew them iid. Same-case twins share the case's difficulty, so counting them as independent understates the uncertainty exactly the way pooling models would -- the 95% CI came out tighter than the case-level evidence supports. Add stats.cluster_bootstrap_ci (draws whole clusters, pools their observations) and resample the solo lane by case_id, so the interval answers "does this hold on new cases", which is the claim the arm makes. The point estimate is unchanged and, since every cluster is a singleton for one-twin-per-case data, the pinned intervals are untouched; only real multi-cue cases widen. --------- Co-authored-by: sebasmos <sebasticajas@gmail.com>
* feat: benchmaxxing run entry point for the stage runners * feat: bootstrap CIs and effect sizes in the run summary * feat: correct the whole family of p-values in the run report * feat: declare model lineage in the config instead of inferring it * feat: noise-floor control and floor-adjusted flip rate * cascade significance: paired permutation on the adoption gap, not a McNemar sign test Agastya's review: _cascade_tests ran the continuous shared/isolated adoption rates through mcnemar by counting only each pair's direction, collapsing it to a sign test that drops ties and ignores magnitude. Over four discordant cases the smallest two-sided p is 0.125, so a unanimous planted-answer effect could never be flagged even when the shared/isolated bootstrap CIs printed right above it are cleanly separated -- the two signals then contradict. Add stats.paired_permutation_test (seeded sign-flip over the per-case differences, exact by enumeration for small n, sampled otherwise) and test the same shared-minus-isolated gap the adoption_delta CI is built on, so the p-value and the interval answer one question. Thread the run seed through significance_tests for reproducibility. * lineage: warn when an org-prefixed id is inferred, not just recorded Agastya's review: the bare-id fallback routes through _lineage, which splits on the first separator, so an org-namespaced id like 'meta-llama/llama-3.1-70b' resolves to 'meta' rather than 'llama'. A mixed-convention roster then gets a wrong within- vs cross-lineage partition in run_holes_test and the run completes clean, with lineage_source: inferred in the manifest as the only trace after the fact. Keep the heuristic as the documented fallback but make the risky case loud: model_specs warns when a lineage is inferred (not declared) from an id carrying a '/'. Declaring `lineage` in the config -- the resolution from #251 -- silences it. Failure is now at run time, not a manifest post-mortem. * solo CI: resample over cases, not twins Agastya's review: the solo lane resampled flip_rate/shortcut_reliance over the flat per-twin record list, but _build_twins emits one twin per (case, cue_type), so a case's several cue-twins sit in that array and bootstrap_ci drew them iid. Same-case twins share the case's difficulty, so counting them as independent understates the uncertainty exactly the way pooling models would -- the 95% CI came out tighter than the case-level evidence supports. Add stats.cluster_bootstrap_ci (draws whole clusters, pools their observations) and resample the solo lane by case_id, so the interval answers "does this hold on new cases", which is the claim the arm makes. The point estimate is unchanged and, since every cluster is a singleton for one-twin-per-case data, the pinned intervals are untouched; only real multi-cue cases widen. * noise floor: match each re-ask to its record by key, not by position Agastya's review: noise_floor_pass zipped a freshly rebuilt _twins(...) against the records run_solo_baselines produced from its own separate _build_twins(...), guarded only by a length check. The two builders are identical today, but the length check catches a count mismatch and not a reordering -- if they ever drift (a limit, a filter, a changed skip, a reorder), the floor would subtract the wrong twin's noise from each flip and the headline floor-adjusted rate would be quietly wrong with nothing failing. Records already carry case_id and cue_type, so key the twins by (case_id, cue_type) and look each record up instead of trusting the two builders to stay byte-for-byte in lockstep. A missing key raises loudly. per_twin stays aligned with records, so the paired floor-adjusted resampling is unchanged. --------- Co-authored-by: sebasmos <sebasticajas@gmail.com>
* feat: benchmaxxing run entry point for the stage runners
* feat: bootstrap CIs and effect sizes in the run summary
* feat: correct the whole family of p-values in the run report
* feat: declare model lineage in the config instead of inferring it
* feat: noise-floor control and floor-adjusted flip rate
* feat: run the imaging lane through the run entry point
* cascade significance: paired permutation on the adoption gap, not a McNemar sign test
Agastya's review: _cascade_tests ran the continuous shared/isolated adoption rates through
mcnemar by counting only each pair's direction, collapsing it to a sign test that drops ties
and ignores magnitude. Over four discordant cases the smallest two-sided p is 0.125, so a
unanimous planted-answer effect could never be flagged even when the shared/isolated bootstrap
CIs printed right above it are cleanly separated -- the two signals then contradict.
Add stats.paired_permutation_test (seeded sign-flip over the per-case differences, exact by
enumeration for small n, sampled otherwise) and test the same shared-minus-isolated gap the
adoption_delta CI is built on, so the p-value and the interval answer one question. Thread the
run seed through significance_tests for reproducibility.
* imaging lane: count dropped findings on multi-finding cases
Agastya's review: _finding did case.label.split('|')[0], so a multi-finding NIH case
(cardiomegaly|effusion) was only ever asked about its first finding and the rest were
dropped with no entry in skipped_cases -- the 'Skipped cases:' line then reported full
coverage the run never had, the exact failure the skip accounting exists to catch.
v1 is one question per case, so keep that policy but make it honest: _positive_findings
lists the real findings, image_twins asks about the first and adds len(rest) to a new
dropped_findings bucket. The skip line already renders any nonzero bucket.
* lineage: warn when an org-prefixed id is inferred, not just recorded
Agastya's review: the bare-id fallback routes through _lineage, which splits on the first
separator, so an org-namespaced id like 'meta-llama/llama-3.1-70b' resolves to 'meta' rather than
'llama'. A mixed-convention roster then gets a wrong within- vs cross-lineage partition in
run_holes_test and the run completes clean, with lineage_source: inferred in the manifest as the
only trace after the fact.
Keep the heuristic as the documented fallback but make the risky case loud: model_specs warns when
a lineage is inferred (not declared) from an id carrying a '/'. Declaring `lineage` in the config
-- the resolution from #251 -- silences it. Failure is now at run time, not a manifest post-mortem.
* solo CI: resample over cases, not twins
Agastya's review: the solo lane resampled flip_rate/shortcut_reliance over the flat per-twin
record list, but _build_twins emits one twin per (case, cue_type), so a case's several cue-twins
sit in that array and bootstrap_ci drew them iid. Same-case twins share the case's difficulty, so
counting them as independent understates the uncertainty exactly the way pooling models would --
the 95% CI came out tighter than the case-level evidence supports.
Add stats.cluster_bootstrap_ci (draws whole clusters, pools their observations) and resample the
solo lane by case_id, so the interval answers "does this hold on new cases", which is the claim
the arm makes. The point estimate is unchanged and, since every cluster is a singleton for
one-twin-per-case data, the pinned intervals are untouched; only real multi-cue cases widen.
* noise floor: match each re-ask to its record by key, not by position
Agastya's review: noise_floor_pass zipped a freshly rebuilt _twins(...) against the records
run_solo_baselines produced from its own separate _build_twins(...), guarded only by a length
check. The two builders are identical today, but the length check catches a count mismatch and not
a reordering -- if they ever drift (a limit, a filter, a changed skip, a reorder), the floor would
subtract the wrong twin's noise from each flip and the headline floor-adjusted rate would be
quietly wrong with nothing failing.
Records already carry case_id and cue_type, so key the twins by (case_id, cue_type) and look each
record up instead of trusting the two builders to stay byte-for-byte in lockstep. A missing key
raises loudly. per_twin stays aligned with records, so the paired floor-adjusted resampling is
unchanged.
---------
Co-authored-by: sebasmos <sebasticajas@gmail.com>
* feat: benchmaxxing run entry point for the stage runners
* feat: bootstrap CIs and effect sizes in the run summary
* feat: correct the whole family of p-values in the run report
* feat: declare model lineage in the config instead of inferring it
* feat: noise-floor control and floor-adjusted flip rate
* feat: run the imaging lane through the run entry point
* feat: hard-case selector for the cascade arm
* cascade significance: paired permutation on the adoption gap, not a McNemar sign test
Agastya's review: _cascade_tests ran the continuous shared/isolated adoption rates through
mcnemar by counting only each pair's direction, collapsing it to a sign test that drops ties
and ignores magnitude. Over four discordant cases the smallest two-sided p is 0.125, so a
unanimous planted-answer effect could never be flagged even when the shared/isolated bootstrap
CIs printed right above it are cleanly separated -- the two signals then contradict.
Add stats.paired_permutation_test (seeded sign-flip over the per-case differences, exact by
enumeration for small n, sampled otherwise) and test the same shared-minus-isolated gap the
adoption_delta CI is built on, so the p-value and the interval answer one question. Thread the
run seed through significance_tests for reproducibility.
* imaging lane: count dropped findings on multi-finding cases
Agastya's review: _finding did case.label.split('|')[0], so a multi-finding NIH case
(cardiomegaly|effusion) was only ever asked about its first finding and the rest were
dropped with no entry in skipped_cases -- the 'Skipped cases:' line then reported full
coverage the run never had, the exact failure the skip accounting exists to catch.
v1 is one question per case, so keep that policy but make it honest: _positive_findings
lists the real findings, image_twins asks about the first and adds len(rest) to a new
dropped_findings bucket. The skip line already renders any nonzero bucket.
* lineage: warn when an org-prefixed id is inferred, not just recorded
Agastya's review: the bare-id fallback routes through _lineage, which splits on the first
separator, so an org-namespaced id like 'meta-llama/llama-3.1-70b' resolves to 'meta' rather than
'llama'. A mixed-convention roster then gets a wrong within- vs cross-lineage partition in
run_holes_test and the run completes clean, with lineage_source: inferred in the manifest as the
only trace after the fact.
Keep the heuristic as the documented fallback but make the risky case loud: model_specs warns when
a lineage is inferred (not declared) from an id carrying a '/'. Declaring `lineage` in the config
-- the resolution from #251 -- silences it. Failure is now at run time, not a manifest post-mortem.
* solo CI: resample over cases, not twins
Agastya's review: the solo lane resampled flip_rate/shortcut_reliance over the flat per-twin
record list, but _build_twins emits one twin per (case, cue_type), so a case's several cue-twins
sit in that array and bootstrap_ci drew them iid. Same-case twins share the case's difficulty, so
counting them as independent understates the uncertainty exactly the way pooling models would --
the 95% CI came out tighter than the case-level evidence supports.
Add stats.cluster_bootstrap_ci (draws whole clusters, pools their observations) and resample the
solo lane by case_id, so the interval answers "does this hold on new cases", which is the claim
the arm makes. The point estimate is unchanged and, since every cluster is a singleton for
one-twin-per-case data, the pinned intervals are untouched; only real multi-cue cases widen.
* hard-case selector: guard --hard-k the way the library guards k
Agastya's review: _select_hard_cases reimplemented the k cut as ranked[:args.hard_k] instead of
routing through select_uncertain_cases, skipping the non-negative guard the library put there for
exactly this. So --hard-k -3 silently kept every case except the three easiest -- the "restricted"
run quietly became nearly the full set at full API cost -- and --hard-k 0 exited with the
misleading "records and the manifest have to come from the same dataset" error instead of naming k.
Reject a negative --hard-k, and treat a k that selects nothing as its own error naming k, before
the manifest-intersection check can mask it. Both flags now fail loudly the way the library
already promises.
* noise floor: match each re-ask to its record by key, not by position
Agastya's review: noise_floor_pass zipped a freshly rebuilt _twins(...) against the records
run_solo_baselines produced from its own separate _build_twins(...), guarded only by a length
check. The two builders are identical today, but the length check catches a count mismatch and not
a reordering -- if they ever drift (a limit, a filter, a changed skip, a reorder), the floor would
subtract the wrong twin's noise from each flip and the headline floor-adjusted rate would be
quietly wrong with nothing failing.
Records already carry case_id and cue_type, so key the twins by (case_id, cue_type) and look each
record up instead of trusting the two builders to stay byte-for-byte in lockstep. A missing key
raises loudly. per_twin stays aligned with records, so the paired floor-adjusted resampling is
unchanged.
---------
Co-authored-by: sebasmos <sebasticajas@gmail.com>
* feat: benchmaxxing run entry point for the stage runners
* feat: bootstrap CIs and effect sizes in the run summary
* feat: correct the whole family of p-values in the run report
* feat: declare model lineage in the config instead of inferring it
* feat: noise-floor control and floor-adjusted flip rate
* feat: run the imaging lane through the run entry point
* feat: hard-case selector for the cascade arm
* feat: per-run reproducibility bundle and the report command
* cascade significance: paired permutation on the adoption gap, not a McNemar sign test
Agastya's review: _cascade_tests ran the continuous shared/isolated adoption rates through
mcnemar by counting only each pair's direction, collapsing it to a sign test that drops ties
and ignores magnitude. Over four discordant cases the smallest two-sided p is 0.125, so a
unanimous planted-answer effect could never be flagged even when the shared/isolated bootstrap
CIs printed right above it are cleanly separated -- the two signals then contradict.
Add stats.paired_permutation_test (seeded sign-flip over the per-case differences, exact by
enumeration for small n, sampled otherwise) and test the same shared-minus-isolated gap the
adoption_delta CI is built on, so the p-value and the interval answer one question. Thread the
run seed through significance_tests for reproducibility.
* imaging lane: count dropped findings on multi-finding cases
Agastya's review: _finding did case.label.split('|')[0], so a multi-finding NIH case
(cardiomegaly|effusion) was only ever asked about its first finding and the rest were
dropped with no entry in skipped_cases -- the 'Skipped cases:' line then reported full
coverage the run never had, the exact failure the skip accounting exists to catch.
v1 is one question per case, so keep that policy but make it honest: _positive_findings
lists the real findings, image_twins asks about the first and adds len(rest) to a new
dropped_findings bucket. The skip line already renders any nonzero bucket.
* lineage: warn when an org-prefixed id is inferred, not just recorded
Agastya's review: the bare-id fallback routes through _lineage, which splits on the first
separator, so an org-namespaced id like 'meta-llama/llama-3.1-70b' resolves to 'meta' rather than
'llama'. A mixed-convention roster then gets a wrong within- vs cross-lineage partition in
run_holes_test and the run completes clean, with lineage_source: inferred in the manifest as the
only trace after the fact.
Keep the heuristic as the documented fallback but make the risky case loud: model_specs warns when
a lineage is inferred (not declared) from an id carrying a '/'. Declaring `lineage` in the config
-- the resolution from #251 -- silences it. Failure is now at run time, not a manifest post-mortem.
* solo CI: resample over cases, not twins
Agastya's review: the solo lane resampled flip_rate/shortcut_reliance over the flat per-twin
record list, but _build_twins emits one twin per (case, cue_type), so a case's several cue-twins
sit in that array and bootstrap_ci drew them iid. Same-case twins share the case's difficulty, so
counting them as independent understates the uncertainty exactly the way pooling models would --
the 95% CI came out tighter than the case-level evidence supports.
Add stats.cluster_bootstrap_ci (draws whole clusters, pools their observations) and resample the
solo lane by case_id, so the interval answers "does this hold on new cases", which is the claim
the arm makes. The point estimate is unchanged and, since every cluster is a singleton for
one-twin-per-case data, the pinned intervals are untouched; only real multi-cue cases widen.
* hard-case selector: guard --hard-k the way the library guards k
Agastya's review: _select_hard_cases reimplemented the k cut as ranked[:args.hard_k] instead of
routing through select_uncertain_cases, skipping the non-negative guard the library put there for
exactly this. So --hard-k -3 silently kept every case except the three easiest -- the "restricted"
run quietly became nearly the full set at full API cost -- and --hard-k 0 exited with the
misleading "records and the manifest have to come from the same dataset" error instead of naming k.
Reject a negative --hard-k, and treat a k that selects nothing as its own error naming k, before
the manifest-intersection check can mask it. Both flags now fail loudly the way the library
already promises.
* report: fail replay when the bundle lost transcripts
Agastya's review: replay_cascade silently continued past any case whose shared/isolated transcript
pair was missing from the RunStore, and _cmd_report built its verdict from the three mean
comparisons alone, never comparing the replayed n_cases to the reported n_cases. So a bundle that
lost part of its transcripts (a partial copy, a pruned directory) still printed no MISMATCH and
exited 0 whenever adoption is pinned -- the replay certified evidence that no longer covered the
run it reported.
replay_cascade now returns the case ids it skipped, and _cmd_report treats a non-empty skip list
or an n_cases shortfall as a coverage MISMATCH before it even looks at the means. A complete bundle
skips nothing, so the happy path and test_replay_reproduces_the_reported_cascade_numbers are
untouched.
* noise floor: match each re-ask to its record by key, not by position
Agastya's review: noise_floor_pass zipped a freshly rebuilt _twins(...) against the records
run_solo_baselines produced from its own separate _build_twins(...), guarded only by a length
check. The two builders are identical today, but the length check catches a count mismatch and not
a reordering -- if they ever drift (a limit, a filter, a changed skip, a reorder), the floor would
subtract the wrong twin's noise from each flip and the headline floor-adjusted rate would be
quietly wrong with nothing failing.
Records already carry case_id and cue_type, so key the twins by (case_id, cue_type) and look each
record up instead of trusting the two builders to stay byte-for-byte in lockstep. A missing key
raises loudly. per_twin stays aligned with records, so the paired floor-adjusted resampling is
unchanged.
---------
Co-authored-by: sebasmos <sebasticajas@gmail.com>
Summary
Added
benchmaxxing run, one command that turns a config plus a manifest into a run directory:The stage logic stays in
experiments.py. The newbenchmaxxing/runner.pyis just the wiring: resolve the config and roster, load cases, inject cues, call the matching runner, writeresults.json,summary.md,config.jsonandrun_manifest.json. The cascade stage also saves every shared and isolated transcript under--out/transcripts/through the existingRunStore.Two small adapters are the only new behaviour, and they are both plumbing.
SoloAgentrenders a twin payload as a lettered MCQ and parses the reply back into an option string (solo_evaluateprefers arun(payload)backend, which is the hook that lets the parse see the twin's own options).CommitteeAgentdoes the same for a blackboard turn with the visible board appended. I deliberately did not touch prompt design, that's #107, so these render the same shape the first real MedQA runs used.Result
All four stages run offline with
--backend mockon a 4-case synthetic manifest:--dry-runon the cascade prints models, lineages, 4 cases / 12 twins, 64 estimated calls, and writes nothingruff check .clean,pytest -q594 passed. New tests drive each stage through the CLI and assert the four output files exist and parse.Notes
Three things worth flagging:
gemini-2.5-flashtogemini,qwen2.5-72b-instructtoqwen) becauseConfighas no lineage field. It's what the overlap arm splits on, so if you'd rather have it explicit in the config, say so and I'll add the field.load_configto read.jsonas well as.yaml. YAML needs the optionalconfigextra, and it felt wrong for the main entry point to require an extra just to pass a roster. YAML still works exactly as before.extract.parse_mcq_choiceonly takes the letter branch when the options themselves are single letters, so with real MedQA-style options a reply of "The answer is B" comes back unparseable. I work around it by parsing against the letters first and falling back to the option text. That's the unification the TODO inextract.py(Answer extraction and abstention normalization for real model outputs #102) is about, so I left the parser alone.The imaging lane raises with a pointer to
experiments/imagingrather than pretending: it needs the images loaded and injected as arrays, which is a bigger piece of work.Closes #101