Repository navigation
Commit c4a2233
authored
feat(evals): independent judge provider/model + fix stale-grade skip on judge swap (#2710)
## What
Two changes to the `hdx-eval` LLM-as-judge grader:
1. **Independent judge provider/model.** The grader can now run on a different provider/model than the run model — e.g. run with Anthropic, grade with OpenAI — to reduce same-model grading bias. `--judge-model` accepts a `provider:model` spec (`anthropic` | `openai`; a bare model name defaults to `anthropic`), configurable per repo via `eval.config.json` `grading.judgeModel`.
2. **Fix a silent stale-grade bug on judge swap.** Re-grading a batch with a *different* judge now actually re-runs that judge instead of returning the previously cached judge's scores.
**Note:** this does **not** change the default judge. The default remains `anthropic:claude-opus-4-7`. This PR only makes an alternate grader *possible* and correct.
## Why
We wanted to measure **grader bias**: does the choice of judge model materially change eval grades, and is one judge more accurate than another? Answering that requires grading the *same* runs with two different judges and comparing.
Before this PR that was impossible in practice:
- The judge was hard-wired to Anthropic.
- Even after making the provider configurable, `needsJudge` and the per-run reuse guard keyed on the **presence** of a cached judge, not its **identity**. So `grade <batch> --judge-model openai:gpt-5.6-sol` over an already-Opus-graded batch would see a cached judge, skip the LLM call, and silently hand back the **Opus** scores relabeled. You'd "compare" two judges and measure nothing — the worst kind of bug, because it fails silently and looks like agreement.
## How
- **Provider resolution** (`src/grading/judgeModel.ts`, new): parses the `provider:model` spec and builds a Vercel AI SDK `LanguageModel`. Credentials come from `AI_API_KEY` / `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` (+ `AI_BASE_URL`, `AI_REQUEST_HEADERS`), mirroring `packages/api`. The **provider-specific key wins** over the generic `AI_API_KEY` so a runner key and a differing grader key don't collide.
- **Judge robustness** (`src/grading/judge.ts`): explicit 0–5 scoring anchors in the prompt (so scores stay calibrated across judge models), structured-output salvage + one retry on schema failure, and a larger output-token budget for reasoning judges (OpenAI `gpt-5.x` / o-series) so hidden reasoning tokens don't truncate the JSON.
- **The fix** (`src/grading/grade.ts`): both `needsJudge` and the per-run reuse guard now compare the cached grade's `judgeModel` against the requested spec. A cached grade from a *different* judge is treated as stale and re-run. `--rerun-judge` still forces a same-judge refresh. Inspection evidence is still reused across judge swaps (re-inspecting after artifact cleanup would fail).
- **Tests**: added regression coverage in `grade.judgeModel.test.ts` — grading one batch with judge A then judge B re-runs B (not skip), and re-grading with the *same* judge still no-ops. Verified the new test fails when the fix is reverted (mutation-checked), so it genuinely guards the regression.
## Eval results — Claude Opus vs GPT-5.6 Sol as judge
Ran the full suite on this branch to exercise the feature and answer the bias question.
**Setup:** 7 scenarios × n=3 = **21 runs**, `hyperdx` MCP, runner `claude-opus-4-6`. Graded the identical batch twice: `anthropic:claude-opus-4-7` (pass A) then `openai:gpt-5.6-sol` (pass B). 0 grading errors either pass.
### Aggregate
| Metric | Opus judge | GPT-5.6 Sol | Δ |
|---|---|---|---|
| Mean judge score (0–1) | 0.721 | 0.605 | **−0.115** |
| Spearman rank correlation | — | — | **0.906** |
| GPT scored ≤ Opus | — | — | **19 / 21 (90%)** |
**GPT-5.6 Sol is systematically ~11 pts stricter, but the two judges rank runs almost identically (0.91).** Practical implication: a *relative* MCP comparison would largely survive a judge swap; *absolute* scores would not (consistent with the README caveat that anchors reduce but don't erase per-model scale differences).
### Per-scenario (mean judge score)
| Scenario | Opus | GPT | Δ |
|---|---|---|---|
| dashboard-build | 0.84 | 0.76 | −0.08 |
| error-root-cause | 1.00 | 0.87 | −0.13 |
| latency-spike | 0.39 | 0.37 | −0.02 |
| metric-saturation | 0.92 | 0.74 | −0.18 |
| noisy-signals | 0.56 | 0.50 | −0.07 |
| segmented-regression | 0.78 | 0.56 | **−0.22** |
| service-health-check | 0.55 | 0.45 | −0.10 |
## How we verified GPT-5.6 Sol is more *accurate* (not just stricter)
"Stricter" is not "better" — a judge that scores everything 0 is strict and useless. Because every criterion stores a `{score, rationale}` and the full `rawResponse`, we could adjudicate each judge's reasoning **against the scenario's documented ground truth**. We reviewed the highest-divergence runs:
**1. `segmented-regression/1` (Opus 0.82 → GPT 0.50).** Ground truth: the regression is at the **enterprise × cache-miss intersection**; single-axis "all enterprise" answers are the designed trap. The agent answered "100% enterprise tenants."
- Opus *noticed* the flaw ("emphasizes 100% enterprise more than the intersection") but still gave correctness **4/5**.
- GPT called it "materially mischaracterizes the scope … rather than only their intersection with cache miss" and gave **3/5**, and docked completeness for omitting the ~12% vs ~0.5% comparison (factually absent from the answer).
- **GPT scored the rubric's actual intent; Opus forgave the exact error the scenario tests for.**
**2. `metric-saturation/1` (Opus 0.93 → GPT 0.64).** The agent attributed pod restarts to "liveness-probe kills" — plausible, but not the seeded mechanism (the heap-leak crash cycle itself).
- Opus gave correctness **5/5**.
- GPT gave **3/5** ("materially misattributes restarts") and additionally caught two distractors the agent ignored (coincidental deploy, stable neighbor) that Opus's rationale missed.
**Control — they agree on clear failures.** On `latency-spike/1` (a truncated `max_turns` fragment), both judges scored ~0.2 and both gave 0/5 on correctness/completeness. GPT is **not** uniformly harsh; the judges converge on genuine failures and diverge only on **borderline-good answers with plausible-but-flawed reasoning**.
**Mechanism (generalizable).** Splitting the Opus-minus-GPT lenience gap by termination:
- `final_answer` runs (n=19): Opus **+0.130** more lenient
- `max_turns` runs (n=2): **−0.025** (even)
Opus's over-scoring is concentrated on **complete-looking answers** — when an answer *reads* authoritative (headers, tables, confident prose), Opus tends to trust the presentation while GPT keeps verifying claims against ground truth.
**Conclusion:** in this sample, GPT-5.6 Sol is the more accurate judge — it catches plausible-but-wrong reasoning that Opus rewards, especially on polished answers. We are **not** changing the default judge yet (n=21; single runner/branch; this measures grader bias, not runner self-preference). This PR makes the alternate grader available and correct so the comparison can be run rigorously at larger n before any default change.
## Caveats
- n=21 — directionally strong and internally consistent, but not statistically heavy. The *mechanism* (Opus rewards plausibility on polished answers) is the robust takeaway; exact deltas will wobble.
- Single runner (`claude-opus-4-6`) and single branch — measures grader bias, not runner self-preference. Opus-runner + Opus-judge is a same-family pairing that could itself inflate Opus leniency; n is too small to isolate that from GPT simply being stricter.
- Reviewed the top divergences, not all 21 pairs.
## Test plan
- `yarn ci:unit` (hdx-eval) — 32 judge/grade tests pass; `tsc --noEmit` clean.
- New regression test mutation-checked: fails when the identity fix is reverted.
- End-to-end: full 21-run suite graded twice with two providers, 0 errors; judge-swap re-ran correctly over cached Opus grades (validating the fix live).1 parent f5f9cd1 commit c4a2233
14 files changed
Lines changed: 1567 additions & 178 deletions
File tree
- .changeset
- packages/hdx-eval
- src
- __tests__
- grading
- hyperdx
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
337 | 337 | | |
338 | 338 | | |
339 | 339 | | |
340 | | - | |
341 | | - | |
342 | | - | |
343 | | - | |
344 | | - | |
345 | | - | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
346 | 346 | | |
347 | 347 | | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
348 | 378 | | |
349 | 379 | | |
350 | 380 | | |
| |||
366 | 396 | | |
367 | 397 | | |
368 | 398 | | |
369 | | - | |
370 | | - | |
371 | | - | |
372 | | - | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
373 | 409 | | |
374 | 410 | | |
375 | 411 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
| 16 | + | |
| 17 | + | |
17 | 18 | | |
| 19 | + | |
18 | 20 | | |
19 | | - | |
| 21 | + | |
| 22 | + | |
20 | 23 | | |
21 | 24 | | |
22 | 25 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
0 commit comments