ENG-1687: pin the verifier eval's served model so an alias repoint cannot pass silently - #434
Conversation
…silently The verdict eval picks its two models by alias NAME. An alias is a catalog pointer, not a model: `mindshub_air` was repointed off Kimi K2.6 to `gpt-5.6-luna` around 2026-08-10, so both slots now hold the same behaviour and the gate has reported green while covering one population twice (ENG-1687). Adds an identity check rather than the behavioural probe the ticket proposed. `LLMResponse.model` is already on every response (ENG-1638), so `_check_served_model` costs no extra call, cannot flake, and catches repoints whose consequence nobody predicted — kimi's Moonshot -> Fireworks move changed no narration but flips its tool_choice failure mode. Behaviour was already guarded by `test_verdict`; identity was the missing half. Watched failing live against prod on today's `mindshub_air` before the pin was set to its current value, and each new unit test watched failing under a mutation of the logic it covers. The ticket's other steps are not done, deliberately: there is no narrating alias left to re-pin the second slot to. Re-measured 2026-09-03, 0 narration characters on eight aliases, identical with `_VERIFIER_NO_PREAMBLE` stripped, and prod's `verifier_failure = 'truncated'` is 0 over 30 days. The docstring now says the matrix covers two provider shapes, which is what is true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l pin Adversarial self-review of #434 at e4f34cc. All three are in the new code. 1. The pin caught the repoint but not the state it CAUSED. ENG-1687 is "both slots ended up holding one model and the gate stayed green" — and the obvious way to clear the pin's red is to update the map to the new id, which lands right back there if that id is the other slot's model. Distinctness now has a test instead of holding by accident, covering both routes in: identical aliases, and distinct aliases pinned to one model. 2. Only the coding provider was shimmed. `from_settings` builds planning and coding as separate objects even when both roles name the same alias, so any future `generate_object` or `chat` call in this file would have been silently unpinned — the same quiet-gap class the pin exists to close. Both providers are wrapped now, deduplicated by identity, each closing over its own inner. 3. `response.model` is remote text reaching an exception message and GITHUB_STEP_SUMMARY, and a newline in it injects lines into a CI artifact — a bogus "✅" row for an alias never checked. Reuses `identity.sanitize_model_name`, which anton already applies to this exact field before it reaches a prompt, rather than the hand-rolled isinstance check. Fix 3 initially passed with the sanitiser removed, so its test was written until it failed for the right reason. Each fix is mutation-verified: pin-to- duplicate and same-alias both red their test, and the sanitiser's removal reds the injection test on both the recorded value and the raise. Verified live that the planning path is now counted (3 raw completes -> 3 guard invocations across 2 verdict calls and 1 `generate_object`) with distinct inner bindings. Full unit suite 2892 passed / 31 skipped; full live eval 13 passed in 84s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review (convention 6)At The deviation, stated plainlyENG-1687 asked for a behavioural population guard — re-measure the property each alias was chosen for, one extra call per alias. This ships an identity pin ( Four reasons, in descending order of how much they should matter to you:
The test that actually closes ENG-1687
ENG-1687 is not "an alias moved" — it is "both slots ended up holding one model and the gate stayed green." That is the coverage-collapse guard, expressed as identity rather than behaviour. If you reject the deviation, this is the test to argue with. Why the second population is not re-pickedThere is nothing to re-pick to. Re-measured 2026-09-03 against prod through this file's own call path ( One clarification worth making explicitly, because the phrasing invites a wrong reading. "0 narration characters" is a claim about this call shape, not about the models in general. Measured on a deliberately short 76-char system prompt instead of the real 439-char verifier prompt,
Two corrections filed against the ticket
Round 2 findings (
|
pnewsam
left a comment
There was a problem hiding this comment.
Code Review
Verdict: APPROVE
Model: claude-opus-4-8[1m]
Reviewed the served-model pin added to tests/test_verifier_verdict_live.py, its unit coverage in tests/test_verifier_eval_gate.py, and the five comment corrections. I traced the shim's actual call path (generate_object_code → _generate_object_with → provider.complete), confirmed the exception design against _verdict's catch clauses, and checked the sanitizer's behavior against every test input. The change is read-only test/comment work, correctly wired, well-tested, and all CI is green. No actionable findings.
Validation
- Confirmed
_pinnedwraps the correct chokepoint: the verdict call routes throughprovider.complete(**kwargs)(all-kwargs,client.py:384), so the shim'sasync def complete(_inner=inner, **kwargs)captures every call;_inner=innerdefault-binds per iteration, so no closure-over-loop-variable bug, and dedup-by-id()handles shared provider objects. - Confirmed
AliasRepointedpropagates as intended:_verdictcatches onlyStructuredOutputError/TokenLimitExceeded("anything else propagates"), and_call_with_auth_confirmationre-raises everything butProviderAuthError— so a repoint fails the eval and cannot be absorbed by the retry/skip paths. The gate test asserts the subclass relationships that guarantee this. - Verified
sanitize_model_namereturnsNonefor non-str/empty (covers the[None, "", 0, object()]case), strips\r\n(the CI-artifact injection defense), and caps at 80 chars — matching every assertion in the injection and missing-id tests. Record-before-raise ordering in_check_served_modelmatches the test that requires a failing run to still report the served id. - Confirmed the eval's only LLM call is
generate_object_code(coding provider); the planning-provider wrap is defensive future-proofing, not dead-but-wrong code. - CI: all checks pass, including
verdict-eval(1m53s) andrun-tests(3m44s). Mergeable, CLEAN.
One out-of-scope note, not a finding: _pinned intentionally does not wrap _router_provider, which is correct because the eval makes no router calls — worth keeping in mind only if a future case adds one.
What
tests/test_verifier_verdict_live.pyselects its two models by alias name (haiku,mindshub_air). An alias is a catalog pointer, not a model — the auth catalog can repoint one with no PR here and no drift detection.mindshub_airwas repointed off Kimi K2.6 togpt-5.6-lunaaround 2026-08-10, so both matrix slots now hold the same behaviour and the gate has been reporting green while covering one population twice.This adds an identity check:
_EXPECTED_SERVEDrecords what each alias resolved to when its slot was last justified, and_check_served_modelverifies it on every response. Plus the served map in the run output and the step summary, so a repoint is visible in the run that first sees it.Why an identity check and not the behavioural probe the ticket specified
The ticket asked for a population guard that re-measures the property each alias was chosen for. Four reasons this is the better shape:
test_verdictasserts the verdicts themselves — a model that starts judging the fixtures differently reds with or without a repoint. Identity was the missing half; a property probe is a second behavioural guard.LLMResponse.modelis on every response since ENG-1638, so nothing extra is called. A stochastic property would need N runs to be trustworthy — this file's own non-determinism policy says one run of a stochastic property is a coin flip, which is why the STUCK case runs 6×.kimimoved Moonshot → Fireworks since the ticket was filed with no narration change at all, but per ENG-1095 that flips itstool_choicefailure mode. A narration probe stays green through it.What is deliberately NOT done
The second population is not re-picked, because there is nothing to re-pick to. Re-measured 2026-09-03 against prod through this file's own call path: 0 narration characters on eight aliases (air, haiku, kimi, deepseek, qwen, glm, grok, gpt-luna), and identically 0 with
_VERIFIER_NO_PREAMBLEstripped — so it is the models that changed, not the prompt suppressing them. Prod agrees:turn_completed.verifier_failure = 'truncated'is 0 over 30 days.Two corrections to the ticket's description, both filed as a comment on ENG-1687:
_build_verify_requestadds no anti-preamble suffix. It does, and has sincefc832d5d(2026-07-27) — three weeks before the sweep it justifies. The conclusion survives the correction; the stated reason for it did not.So the docstring now describes the matrix as two provider shapes (Anthropic-native
haiku, OpenAI-nativemindshub_air), which is what is true, with the dead rationale kept as dated history so nobody restores it without re-measuring.Verification
mindshub_air— the pin set tokimi-k2p6(what the old docstring assumed) raisesAliasRepointednaming both ids. That satisfies the ticket's "watched failing before any re-pick"Done when.printis captured and discarded on a passing test, so it only showed on red runs) andGITHUB_STEP_SUMMARY.Comments corrected
Five sites asserted in the present tense that
mindshub_airnarrates, which is the belief that produced this bug:anton/core/session.py,anton/core/llm/structured.py,anton/core/llm/provider.py,anton/core/memory/consolidator.py,anton/core/memory/cortex.py. The measurements are kept as dated history — they are why the budget ladders are what they are, and deleting the rationale would invite someone to lower them.provider.py:76already documented the repoint correctly and is untouched.Security check
Performed. Read-only test change plus comment edits: no new endpoint, no new credential, no auth surface, no change to any request anton sends in production. Uses the existing
MINDSHUB_API_KEYCI secret from ENG-1334._check_served_modelinterpolates the gateway-reportedmodelstring into an exception message only — it is never written to disk, never sent anywhere, and the step-summary write is wrapped so an unwritable summary cannot fail a green eval. Fixtures are authored, not captured production payloads.Out of scope
supports_named_tool_choicecapability instead of a name — the version that stops rotting, perregistry.py:117. Different ticket.Closes ENG-1687.