2026-05-20-phase-8-4-verifier-reward-hook
2026-05-20 — Verifier → bandit reward wiring (Phase 8.4)
Summary
Wires Phase 8.3's verifyContract into the bandit reward path so that responses violating a declared contract demote the model in Thompson posterior weights. Also emits X-BR-Contract-Status and X-BR-Contract-Failure response headers on the non-streaming path.
What changed
evaluateOutcomeV2extended with a newcontractVerifierinput. Newcontract_failedclassification (validity 0) supersedes the existing JSON heuristic when set, but is correctly ordered below upstream_error / null_content / tool_use_success (hard failures and correct shapes win).
maybeApplyRewardV2helper (inmodel-router.ts) now runsdetectContract+verifyContractinline when the feature flag (BR_BANDIT_REWARD_V2=1) is on. Verifier failure is non-fatal — wrapped in try/catch so a verifier bug doesn't crash the reward path.
- Messages threaded through the helper at the two non-streaming call sites (the third call site,
scoreAndRecordValidity, doesn't yet receive messages — documented as a follow-up; enum and exact_string verifiers will benot_applicablefrom that path).
- Header emission added to non-streaming handler:
X-BR-Contract-Status: passed|failed|not_applicableandX-BR-Contract-Failure:when the verifier produced a verdict.
What didn't change
- Streaming handler headers. Streaming responses commit headers before the first byte; verifier can't run inline (response isn't assembled yet). The bandit reward signal still updates (verifier runs in
maybeApplyRewardV2after stream completion), just no Contract-Status header on streaming responses. Could be added as a trailer header in a follow-up. - Cache admission gate. Phase 2's gate still only checks structural failures. Hooking the verifier into cache STORE (so contract-failed responses are also rejected at store-time, not just lookup-time) is a clean follow-up but not in this PR.
- scoreAndRecordValidity messages. That signature accepts
toolsbut notmessages— refactoring to pass messages is a follow-up; affects only enum + exact_string verifiers in the audit-chain path.
Decision order in evaluateOutcomeV2 (final)
- Upstream 5xx
- Executor null-content (cascade already engaged)
- Structural null-content / empty-truncation
- Tool-use success (
content: null+tool_callsis correct shape) - Contract verifier failed ← NEW in Phase 8.4
- JSON-contract heuristic (best-effort fallback when verifier wasn't run)
- Refusal
- Success
Verification
$ npx vitest run --config vitest.unit.config.ts src/router/bandit-reward-v2.test.ts
✓ 39 passed (8 new for Phase 8.4 contract verifier wiring)
$ pnpm tsgo
✓ no errors
Coverage of new path:
- Verifier
failedon usable response →contract_failed(validity 0) - Verifier
passedandnot_applicableare no-ops - Upstream_error / null_content / tool_use_success all WIN over verifier failure (priority ordering)
- Verifier failure supersedes the JSON heuristic
- Absent contractVerifier leaves V2 behavior unchanged
Files
src/router/bandit-reward-v2.ts— new classification + dispatcher branch +VALIDITY_CONTRACT_FAILEDconstantsrc/router/bandit-reward-v2.test.ts— 8 new testssrc/router/model-router.ts—maybeApplyRewardV2runs verifier + threadsmessages; call sites updatedsrc/api/routes/completions/non-streaming.ts—X-BR-Contract-Status+X-BR-Contract-Failureheaders- Ship-log (this file)
Operational impact
- Feature-flagged.
BR_BANDIT_REWARD_V2=1(already set per ECS task def for canary; off by default per Phase 4) gates ALL the new behavior. When off, Phase 8.4 is a no-op. - Bandit demotion. When V2 is on AND a contract-strict request gets a parseable-but-wrong response, the bandit records
validity=0instead ofvalidity=1. Over time, models that repeatedly fail contracts for a given task class get demoted in the Thompson posterior. - No latency cost in the V2-off path. When the feature flag is off, the verifier never runs.
Lockstep
- [x] No API surface change beyond additive headers.
- [x] SDKs unaffected.
- [x] MCP tools unaffected.
- [x] Ship-log (this file).
Plan reference
Phase 8.4 of the contract-aware-routing arc. Depends on Phase 8.3 (#382). Phase 8.5 (MCP gating) is the next milestone.