Skip to content

feat(pricing): monitor the rate table against two public sources - #129

Open
c-1k wants to merge 20 commits into
masterfrom
ship/pricing-drift
Open

feat(pricing): monitor the rate table against two public sources#129
c-1k wants to merge 20 commits into
masterfrom
ship/pricing-drift

Conversation

@c-1k

@c-1k c-1k commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What

PRICING_TABLE ships to customers in the published usertrust package and had no drift monitoring of any kind. PRICING_TABLE_VERSION is a hand-typed constant; nothing fetched, compared, or expired.

This matters beyond hygiene. receipt.pricing.appliedRates + tableVersion are what let a third party recompute a metered cost from the record alone — check out the stamped table version, recompute ceil(sum(counts × rates / 1000)), floored at 1. That the table is Apache-2.0 public source is what makes independent verifiability real rather than rhetorical. So a stale rate produces receipts that are internally consistent, cryptographically sound, and externally wrong — and the hash chain preserves them faithfully.

Measured against the shipped table

Re-measured live 2026-08-23T20:48:02Z against PRICING_TABLE_VERSION 2026-08-10:

Outcome Count
Exact agreement, all four tiers 15
Expected deviation (ours higher) 1 — claude-sonnet-5
Sources conflict with each other 3 — deepseek-chat, deepseek-reasoner, gpt-5.6-sol
No upstream source at all 7
Total 26

Coverage 36/36 source→model mappings, across 19/19 mapped models. Exit status is 0.

Nothing in the table is understated. One entry is now known stale in the safe direction: gpt-5.6-sol carries 50/300 while OpenAI publishes 40/200 promotional through 2026-11-21. Report-only stays report-only, so that rate is adjudicated in #142 rather than changed here.

Five rules carry the weight

Vendor pinning. models.dev carries 190 providers, most of them resellers. An unpinned matcher answered claude-fable-5 at 30/185 against a true 100/500, and claude-opus-5 at 55/275 (a regional premium) — every one well-formed and wrong. Both sources are pinned to the vendor; a non-vendor row is treated as absent, never as an answer. Mapping is explicit per model, because command-a would otherwise fuzzy-match command-a-translate-08-2025.

Deviations only in the safe direction. Both sources publish claude-sonnet-5 at $2/$10 — the introductory rate through 2026-08-31 that pricing.ts deliberately does not carry. So "differs from upstream" must not mean "we are wrong", or the monitor's first act would be to undo a deliberate conservative choice. An allowlist entry suppresses failure only while our rate is ≥ upstream; if upstream ever rises above ours it fails despite the entry. Understatement is never allowlistable — which enforces the D1 invariant rather than restating it.

Exit 2 is never 0. "Could not check" and "checked, found nothing" are different states. A coverage floor derived from the map (not a transcribed constant, so it self-maintains) turns a silent upstream schema change into a failure instead of a clean sweep of nothing.

Only agree is silent. Every model gets exactly one outcome, the counts must sum to the table size, and every other outcome reports by construction — including the 7 models with no external check, which is the most useful thing the report says.

Catalogs vote once, not N times. LiteLLM and models.dev are not independent: both derive from the providers' published pages and both lag promotional changes, so N of them agreeing is one upstream fact with N mirrors. Measured 2026-08-22, both catalogs and this table priced gpt-5.6-sol at 50/300 while OpenAI's page said 40/200 — acting on "three-way agreement" would have overcharged output by 50% on the most-used model, with three sources apparently confirming it. Within hours LiteLLM moved to 40/200 while models.dev did not, so the agreement was visibly a timing artefact. The report now carries evidence, a count of distinct evidence tiers under an explicit billed > provider > catalog precedence, and reads "litellm, models.dev — 1 evidence tier" instead of implying two confirmations.

Testing

78 tests, no network. The suite carries a positive control (a rate mutated below upstream that must be reported) beside a negative control — an all-pass suite cannot detect an instrument that is disconnected. The exit-2 path is tested by stubbing fetch, not by blocking the network: Node's fetch ignores https_proxy, so a proxy-based probe succeeds and proves nothing.

The unit conversion is pinned to a real table entry (PRICING_TABLE["claude-opus-5"].inputPer1k) rather than a literal, because assert.equal(convert(5), 5) passes against a broken identity conversion and locks the error in.

Scope

Report-only: contents: read means GitHub refuses a push, so no upstream value can reach pricing.ts through this path. Does not touch the receipt shape, TrustReceipt, or shared/types.ts. No cross-repo token.

Pure decision-rule tests run in ci.yml; the live comparison stays weekly so no network fetch can flake a PR.

Deferred findings (not in this PR)

  1. resolveRates prefix-matching understates silently — tracked in pricing: resolveRates prefix match understates silently and reports rateSource "table" #143. claude-opus-5-fast matches claude-opus-5, prices at standard rates (2× understatement — fast is $10/$50), and returns rateSource: "table", indistinguishable on the receipt from an exact match. Regional variants are safe only by luck: us.anthropic.… doesn't prefix-match, so it lands on FALLBACK_RATE with unknown: true — visible, and it fires unknownModelPolicy. The suffix case is the silent one.
  2. No regional dimension. LiteLLM carries us./eu./au./jp. at a 10% premium.
  3. 7 of 26 shipped rates have no external corroboration. Reported from day one; closing the gap is separate work.
  4. gpt-5.6-sol is stale in the safe direction — tracked in pricing: adjudicate gpt-5.6-sol — table carries 50/300, OpenAI publishes 40/200 promotional through 2026-11-21 #142. Caught by this monitor on its own table; the promotional window through 2026-11-21 makes "correct" time-dependent, so it needs a decision rather than a drive-by edit.

c-1k and others added 2 commits August 18, 2026 06:36
PRICING_TABLE ships to customers in the published package and had no drift
monitoring of any kind: PRICING_TABLE_VERSION is a hand-typed constant, and
nothing fetched, compared, or expired. A stale rate does not merely
overcharge — receipt.pricing.appliedRates plus tableVersion are what let a
third party recompute a metered cost from the record alone, so a wrong rate
produces receipts that are internally consistent, cryptographically sound,
and externally wrong, and the hash chain preserves them faithfully.

Adds a weekly checker that cross-checks all 26 models against LiteLLM and
models.dev (both MIT). It REPORTS ONLY — `contents: read` means GitHub
refuses a push, so no upstream value can reach pricing.ts through this path.
Findings land in one deduped issue that closes when a run comes back clean.

Measured on the shipped table: 16 exact four-tier agreements, 1 expected
deviation, 2 cross-source conflicts, 7 models with no upstream source.

Four rules carry the weight:

- VENDOR PINNING. models.dev carries 190 providers, mostly resellers; an
  unpinned matcher answered claude-fable-5 at 30/185 against a true 100/500
  and claude-opus-5 at a regional premium. Both sources are pinned to the
  vendor and a non-vendor row is treated as absent, never as an answer.
  Mapping is explicit per model — `command-a` would otherwise fuzzy-match a
  translation model's rate.

- DEVIATIONS ONLY IN THE SAFE DIRECTION. Both sources publish
  claude-sonnet-5 at the introductory rate the table deliberately does not
  carry, so "differs from upstream" must not mean "we are wrong". An
  allowlist entry suppresses failure only while our rate is >= upstream;
  understatement is never allowlistable. That enforces the D1 invariant
  rather than restating it.

- EXIT 2 IS NEVER 0. "Could not check" and "checked, found nothing" are
  different states. A coverage floor derived from the map (not a transcribed
  constant) turns a silent upstream schema change into a failure instead of
  a clean sweep of nothing.

- ONLY `agree` IS SILENT. Every model is assigned exactly one outcome, the
  counts must sum to the table size, and every other outcome reports by
  construction.

The suite carries a positive control — a rate mutated below upstream that
MUST be reported — beside a negative control. An all-pass suite cannot
detect an instrument that is disconnected. The unit conversion is pinned to
a real table entry rather than a literal, because a test asserting
convert(5) === 5 passes against a broken identity conversion.

The pure decision-rule tests also run in ci.yml; the live comparison stays
weekly so no network fetch can flake a PR.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
Review of the initial implementation found four P1s and a P2. Three of the
P1s were one underlying mistake — electing a single source as "the"
consensus — and the fourth was the monitor exhibiting, in its own
instrument, the silent degradation it exists to detect.

Comparison is now per-tier across EVERY source, and understatement is
proven against the minimum upstream value.

- Omitted cache tiers no longer assumed conservative. An absent field
  meters at inputPer1k via the D1 fallback, which is conservative for a
  cache-READ discount (~0.1x input) but not for a cache-WRITE premium
  (1.25x input), where it understates by 20%. Understatement is judged on
  the effective metered rate, never on field presence. No live instance in
  the table today; it would have passed silently the day one appeared.

- Understatement survives cross-source conflict. Previously any conflicting
  tier skipped all comparison, so ours=50 against sources 60 and 70 exited
  0 — low on either reading. Conflict now suppresses only the value
  proposal, never the understatement check.

- Tiers merge across sources. sourcesAgree treated a missing tier as
  compatible while consensus came from one source, so a tier only the other
  source published was discarded entirely, and reversing source order
  changed the verdict. Tested for order-independence.

- Schema sentinels validate the fetched corpora BY NAME at ingest. The old
  "schema pin" test compared a constant to a hardcoded copy of itself and
  never touched upstream data. A renamed optional field would leave
  input/output resolving, every row answered, coverage full, and the run
  exiting 0 with cache comparison silently disabled. Removed the
  tautological test rather than leave it reading as coverage.

- Cache gaps render for agreeing models, which have no section of their own
  and so previously produced no row at all.

Live verdict is unchanged (16 agree, 1 expected deviation, 2 source
conflicts, 7 uncorroborated, exit 0): these close latent holes rather than
change today's answer. 40 tests, still no network in the suite.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
@c-1k

c-1k commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Codex review — round 1

codex review --base origin/master
model: gpt-5.6-sol (max effort)
REVIEWED_SHA: 378988e219b117012edce1d97282946f6870ee92
EXIT: 0

Verdict: REVISE — 4 P1, 1 P2.

Several comparison paths can report a clean result despite proven or possible understatement, including cache-write premiums and source conflicts. Cache schema loss and cache-gap reporting can also disappear silently, undermining the monitor's core contract.

The review named the changed files (103 references), so this is a real pass, not a no-op against an empty diff.

Findings

Finding File
P1 Fail omitted cache tiers that underprice premiums scripts/pricing-drift/compare.mts:221-226
P1 Preserve understatement checks across source conflicts scripts/pricing-drift/compare.mts:188
P1 Merge nonconflicting tiers from every source scripts/pricing-drift/compare.mts:212
P1 Validate cache fields against upstream schema changes scripts/pricing-drift/sources.mts:112-113
P2 Render cache gaps for otherwise agreeing models scripts/pricing-drift/report.mts:125

All five accepted as real and fixed in 86929ec. Three of the four P1s were one underlying mistake — electing a single source as "the" consensus rather than resolving each tier across all sources — so the fix was structural rather than five patches.

The two worth calling out

The monitor had, in its own instrument, the defect it exists to detect. The "schema pin" test asserted LITELLM_FIELDS against a hardcoded copy of itself. A constant compared to itself says nothing about upstream. Had LiteLLM renamed cache_creation_input_token_cost, input and output would still have resolved, every row would still have counted as answered, coverage would have stayed at 19/19, and the run would have exited 0 with cache comparison silently disabled. Replaced with runtime sentinels that validate both fetched corpora by name at ingest and exit 2; the tautological test was deleted rather than left reading as coverage.

An assumption carried over from the design was wrong. I had written that an omitted cache tier is always conservative, because it meters at inputPer1k through the D1 fallback. That holds for a cache-read discount (~0.1x input). It is false for a cache-write premium (1.25x input), where the fallback understates by 20%. Understatement is now judged on the effective metered rate rather than on field presence. Probed the live table: no model currently omits a cache tier that upstream prices above its input rate, so this was latent — it would have passed silently the day one appeared.

Note packages/core/src/ledger/pricing.ts:58 makes the same overbroad claim in its own comment ("priced at inputPer1k — conservative overstatement"). Not touched here; flagged as a follow-up.

Live verdict unchanged after the fixes: 16 agree, 1 expected deviation, 2 source conflicts, 7 uncorroborated, exit 0. Round 2 running against 86929ec.

…trument

Second review round: 3 P1, 4 P2, none of them repeats.

The first P1 is the previous round's fix reappearing one abstraction
higher. Round 1 stopped a source conflict from masking UNDERSTATEMENT but
left it masking plain DISAGREEMENT: ours 75/250 against sources 50/200 and
50/250 returned a non-failing source-conflict while input was a unanimous,
unallowlisted mismatch. Definitive diffs are now classified before conflict;
conflict wins only when every diff is conflicted.

- The workflow could fail silently. If `drift:test` failed, every later step
  was implicitly gated on success(), so the tracking-issue step was skipped
  and the monitor merely turned red — recording nothing, which is the option
  this design explicitly rejected. The reporting step is now `always()`
  gated, carries the self-test outcome, and says which stage broke. Noted in
  place why this is not the `always()` anti-pattern: that rule governs steps
  reporting SUCCESS, and here success() is what breaks it.

- The schema sentinel counted property NAMES. An upstream keeping a
  deprecated key holding null while moving the numeric price elsewhere would
  clear the floor while the normalizer dropped every value. It now counts
  usable typed values.

- A process death before run.mts writes its report yields status 1, 127 or
  137 with no file; the drift branch would then cat a missing file and abort
  under set -e, producing no issue. Drift now requires exit 1 AND a non-empty
  report; everything else routes to the could-not-run body.

- effectiveRate duplicated D1's fallback instead of calling
  resolveAppliedRates, and had already diverged: a negative finite cache rate
  meters at inputPer1k in the SDK but compared as negative here. AGENTS.md
  forbids a second resolution site, and a monitor that resolves rates
  differently from the thing it monitors measures the wrong quantity.

- Tier consensus keeps min AND max. An omitted tier is benign only when it
  meters at or above EVERY source; between min and max it is above one and
  below another, so it is reported rather than filed as safe. Conflicted
  tiers render as a range.

- --json now carries orphanDeviations and a merged failed flag, so an
  orphan-only failure is no longer a non-zero exit beside `failed: false`.

Live verdict unchanged: 16 agree, 1 expected deviation, 2 source conflicts,
7 uncorroborated, exit 0. 44 tests, no network.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
@c-1k

c-1k commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Codex review — round 2

codex review --base origin/master
model: gpt-5.6-sol (max effort)
REVIEWED_SHA: 86929ec2466bd3e9d9708c33672339f14c332e9f
EXIT: 0

Verdict: REVISE — 3 P1, 4 P2. No repeats from round 1.

The checker has false-clean paths and workflow failures that bypass its tracking issue. It can also silently stop validating cache tiers after certain schema changes.

211 references to the changed files, so a real pass.

Findings

Finding File
P1 Evaluate definitive diffs before source conflicts compare.mts:276
P1 File an issue when the checker tests fail pricing-drift.yml:75
P1 Validate schema values, not just property names sources.mts:125
P2 Handle pre-report process failures as unchecked runs pricing-drift.yml:83
P2 Reuse the canonical cache-rate resolver compare.mts:143
P2 Preserve the full source range for conflict reports compare.mts:157
P2 Include orphan failures in JSON output run.mts:101

All seven accepted and fixed in 2cd264b.

The one worth dwelling on

The first P1 is round 1's fix reappearing one abstraction higher. Round 1 stopped a source conflict from masking understatement. It did not stop a conflict from masking plain disagreement: ours 75/250 against sources 50/200 and 50/250 returned a non-failing source-conflict while input was a unanimous, unallowlisted mismatch at 50. Same defect, same file, one level up — the abstraction introduced to fix a defect class was where that class reappeared. Definitive diffs now classify before conflict; conflict wins only when every diff is conflicted.

Two others were the monitor failing to monitor itself. If drift:test failed, every later step was implicitly gated on success(), so the tracking-issue step was skipped and the run merely turned red — recording nothing, which is precisely the "fail the job only" option this design rejected up front. The reporting step is now always()-gated and names which stage broke. That is not the always() anti-pattern it resembles: that rule governs steps reporting success (a healthcheck ping gated always() reads healthy for failed runs). This step reports failure, so success() is what breaks it. Called out in a comment so it doesn't get "fixed" later.

The schema sentinel counted property names, so an upstream keeping a deprecated key holding null while moving the price elsewhere would have cleared the floor while the normalizer dropped every value. It now counts usable typed values.

effectiveRate also duplicated D1's fallback rather than calling resolveAppliedRates, and had already diverged — a negative finite cache rate meters at inputPer1k in the SDK but compared as negative here. AGENTS.md forbids a second resolution site, and a monitor that resolves rates differently from the thing it monitors is measuring the wrong quantity.

Live verdict unchanged across all three rounds: 16 agree, 1 expected deviation, 2 source conflicts, 7 uncorroborated, exit 0. 44 tests, no network. Round 3 running against 2cd264b.

c-1k and others added 2 commits August 18, 2026 07:29
Third review round: 1 P1, 2 P2. Findings are converging (5 → 7 → 3).

The P1 is the coverage floor at the wrong granularity. It counted MODELS
with at least one answering source, so if LiteLLM renamed `gpt-4o` while
models.dev still answered, the model stayed "corroborated", coverage held
at 19/19, and the run came back clean — with one of the two independent
checks silently gone. The whole point of two sources is that they are
independent; a gate that cannot notice one disappearing is not guarding
that. Coverage is now counted over (source, model) pairs: 36/36 today,
where the model-level view showed 19/19.

- The source-conflict note claimed our rate was "not below any of them".
  With sources at 50 and 70 and ours at 60 that is false, and since the
  outcome does not fail, the note actively reassured about a possible
  underestimate. It now says only that no definitive value can be selected,
  plus the one thing that is true: we are not below the lowest.

- A conservative omitted tier on a CONFLICTED tier recorded only a cache
  gap, so the model was still classified source-conflict but rendered "—",
  hiding the values that caused the conflict. The range is now retained
  alongside the gap.

Live verdict unchanged for the fourth consecutive round: 16 agree, 1
expected deviation, 2 source conflicts, 7 uncorroborated, exit 0. 48 tests,
no network.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
…nels

Fourth review round: 3 P1, 2 P2.

The sharpest inverts a claim made two commits ago. The coverage floor was
praised for being DERIVED from MODEL_MAP and therefore self-maintaining —
but a gate computed from the thing it guards is not a gate. Setting one
model's `litellm` to `null` lowers the actual and expected counts together,
so 35/35 passes and an independent check is lost with nothing moving. Both
checks now run: the derived one catches a source dropping a model, and
checked-in MIN_MAPPINGS / MIN_CORROBORATED_MODELS catch the map being
weakened. Lowering either is an explicit, reviewable edit, which is the
point. Floors are passed in rather than hardcoded so compareTable stays
drivable from fixtures.

- A rate ABOVE every conflicting source is definite drift. Sources at 50 and
  70 with our table at 100 marked every diff conflicted and exited 0, though
  no source supports 100. Only a rate INSIDE the range is undecidable.

- Schema sentinels are scoped to mapped vendor rows. A corpus-wide count is
  satisfied by rows this tool never reads: a mapped provider could rename a
  cache field while a few unrelated or reseller rows kept the old name, and
  the sentinel would pass while every mapped row normalized that tier away.
  Counted over the 17 mapped LiteLLM rows instead.

- Missing mappings are NAMED. The banner said 35/36 and, since agreeing rows
  are omitted, never identified which pair vanished.

- gh commands set GH_REPO. A failed checkout leaves no git remote, so the
  always() reporting steps reached `gh issue list`, which could not infer the
  repository and exited under set -e — filing no issue for precisely the
  could-not-run case that most needs one.

Live verdict unchanged for the fifth consecutive round: 16 agree, 1 expected
deviation, 2 source conflicts, 7 uncorroborated, exit 0, 36/36 mappings.
52 tests, no network.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
@c-1k

c-1k commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Codex review — rounds 3 and 4

round 3  REVIEWED_SHA: 2cd264b33d39e0cc3d94b98e2a7bc371c7f8c576  EXIT: 0  → 1 P1, 2 P2
round 4  REVIEWED_SHA: 23009a8cddc88f6a944abb2226e21e40292b9e4a  EXIT: 0  → 3 P1, 2 P2

Both passes named the changed files (113 and 213 references). All seven findings accepted and fixed — round 3 in 23009a8, round 4 in 4b36ce5.

Round 3 — 1 P1, 2 P2

The checker can silently lose an explicitly mapped source while retaining full coverage, and two conflict paths produce incomplete or inaccurate reports.

Finding
P1 Enforce coverage for every mapped source
P2 Avoid claiming an in-range rate exceeds every source
P2 Preserve the range for conservative cache conflicts

The P1 was the coverage floor at the wrong granularity. It counted models with at least one answering source, so if LiteLLM renamed gpt-4o while models.dev still answered, the model stayed corroborated, coverage held at 19/19, and the run came back clean — with one of two independent checks silently gone. The entire value of two sources is independence; a gate that cannot notice one disappearing is not guarding that. Coverage is now counted over (source, model) pairs: 36/36 today, where the model-level view showed 19/19.

Round 4 — 3 P1, 2 P2

The monitor can emit a clean result for definite rate drift and can silently redefine or skip portions of its coverage.

Finding
P1 Flag rates above every conflicting source
P1 Keep a fixed coverage floor outside MODEL_MAP
P1 Scope schema sentinels to mapped vendor rows
P2 Report which source-model mappings disappeared
P2 Set GH_REPO for failure-reporting commands

The second P1 inverts a claim I made in the round-3 commit. I described the derived coverage floor as self-maintaining and treated that as a virtue. It is the weakness: a gate computed from the thing it guards is not a gate. Setting one model's litellm to null lowers the actual and the expected count together, so 35/35 passes and the lost check closes the issue. Both now run — the derived expectation catches a source dropping a model, and checked-in MIN_MAPPINGS / MIN_CORROBORATED_MODELS catch the map itself being weakened. Lowering either is an explicit, reviewable edit.

The sentinel scoping is the same shape one level down: counting fields across the whole 3,040-row corpus is satisfied by rows this tool never reads, so a mapped provider could rename a cache field while a handful of reseller rows kept the old name. Now counted over the 17 mapped LiteLLM rows.

Where this stands

Five rounds, 20 findings, every one accepted as real. The live verdict has been byte-identical through all of them — 16 agree, 1 expected deviation, 2 source conflicts, 7 uncorroborated, exit 0. Every fix closed a path by which a future run could report clean while something was wrong; none changed today's answer. That is the expected shape for this kind of tool, and it is why the review rounds were worth spending.

52 tests, no network in the suite. Round 5 running against 4b36ce5.

Recurring theme worth recording: three separate rounds found the same defect at successively higher levels of abstraction — conflict masking understatement (r1), conflict masking disagreement (r2), then a rate above every source still classified as conflict (r4). And twice the monitor carried, in its own instrument, the failure mode it exists to detect: a schema test that compared a constant to itself, and a coverage floor derived from the map it was guarding.

…o master

Fifth review round: 1 P1, 3 P2.

The P1 is the schema sentinel's floor being shared across fields. A single
number is cleared by unaffected rows while a PARTIAL rename — one provider,
or one field — strips the tier from every other mapped row. Those rows still
answer, because input and output survive, so mapping coverage stays full and
a changed cache rate can be classified `agree`. Worst on a one-source model
like kimi-k3, where nothing else can contradict it.

Replaced with per-source, per-field baselines measured against the mapped
rows (litellm 17/17/17/17/8, models.dev 19/19/18/8). They are a floor, not an
equality: upstream adding cache rates is fine, losing them is not, and a
legitimate drop is a deliberate edit. Mutation-tested against the live feeds:
raising a baseline produces exit 2 with the field named.

- Range classification now uses the EFFECTIVE rate. A cacheReadPer1k of -5
  resolves to inputPer1k under the canonical rule, so comparing raw -5 to
  conflicting sources at 5 and 10 read as in-range and passed while the SDK
  was charging 50.

- Absolute floor breaches are carried on the report. Both counts can equal
  their derived expectation while sitting below the checked-in floor, with no
  missing mappings and every model showing `agree` — the workflow would then
  open a drift issue containing no failing reason.

- Issue mutation is restricted to the default branch. workflow_dispatch
  accepts any ref and checkout honours it, but the tracking issue is
  repository-wide: a clean feature branch would close an issue describing
  drift on master, and a dirty one would overwrite it. Non-default refs still
  run the check and still fail the job; they no longer rewrite shared state.

Live verdict unchanged for the sixth consecutive round: 16 agree, 1 expected
deviation, 2 source conflicts, 7 uncorroborated, exit 0, 36/36 mappings.
55 tests, no network.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
@c-1k

c-1k commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Codex review — round 5

REVIEWED_SHA: 4b36ce5d8179077c1958d32e1b5cdf4adb91df68
EXIT: 0   →  1 P1, 3 P2   (163 references to the changed files)

The checker can silently lose cache-tier coverage and still exit successfully.

Finding
P1 Pin cache-field coverage at the mapped-row baseline
P2 Classify conflicts using the effective cache rate
P2 Expose absolute-floor breaches in the report
P2 Keep non-default dispatches from rewriting the tracking issue

All four fixed in df416db.

The P1 is the schema sentinel's floor being shared across fields. One number is cleared by unaffected rows while a partial rename — one provider, or one field — strips the tier from every other mapped row. Those rows still answer, because input and output survive, so coverage stays full and a changed cache rate reads as agree. Worst on a one-source model like kimi-k3, where nothing else can contradict it.

Now per-source, per-field baselines measured against the mapped rows (litellm 17/17/17/17/8, models.dev 19/19/18/8), as a floor rather than an equality — upstream adding cache rates is fine, losing them is not.

Mutation-tested against the live feeds rather than only fixtures. Raising one baseline from 8 to 99 produces:

EXIT=2
LiteLLM: field coverage fell below the recorded baseline — schema likely changed:
  cache_creation_input_token_cost (8 of 99 mapped rows). Refusing to report a
  comparison that silently skipped these tiers.

That is a positive control on the real path, not just in the suite.


Stopping point

Six rounds, 24 findings, every one accepted as real: 5 → 7 → 3 → 5 → 4. Round 6 is running against df416db. My intent is to treat it as the last gate: fix anything P1, and ledger any remaining P2/P3 as residuals in this PR rather than continue indefinitely. This is a report-only tool — contents: read means it cannot write to pricing.ts under any failure mode — so the residual risk of a narrow edge case is a missed alarm, never a wrong rate.

The live verdict has been byte-identical through all six rounds — 16 agree, 1 expected deviation, 2 source conflicts, 7 uncorroborated, exit 0, 36/36 mappings. Not one fix changed today's answer; each closed a path by which a future run could report clean while something was wrong. For a monitor, that is the whole product, and it is why the rounds were worth spending.

Two patterns are worth recording beyond this PR:

The same defect kept reappearing one abstraction higher. Conflict masking understatement (r1) → conflict masking disagreement (r2) → a rate above every source still classified as conflict (r4) → a malformed rate compared raw instead of effective (r5). Four rounds, one idea: a comparison that cannot conclude something must not thereby conclude nothing is wrong.

Twice the monitor carried the exact failure mode it exists to detect. A schema test that compared a constant to itself, and a coverage floor derived from the map it was guarding. Both read as coverage while providing none — which is the definition of the thing this tool was built to catch.

Sixth review round: 1 P1, 2 P2. The first two attempts at this gate died
without a verdict (one on a malformed shell command of its own, one cut off
mid-investigation); neither was read as clean. The third produced findings.

The P1 is the third narrowing of the same check, and the previous two were
both still too coarse. A shared floor is cleared by unaffected rows.
Per-field totals are only a LOWER BOUND, so an upstream ADDITION creates
headroom: once one row gains cache_write, a later loss on a different row
keeps the total at the baseline and passes, while that row still answers
from input/output and the comparison silently skips its missing tier.

Replaced with a per-(source, model, tier) expectation — 51 entries measured
against the live feeds. A specific model losing a specific tier is now a
specific, named failure. Mutation-tested end to end: removing kimi-k3's
cache_write expectation-holder yields exit 2 with "kimi-k3 lost cache_write"
rather than a count that still sums correctly.

- A non-finite required rate reported `agree`. rawTier rejects Infinity and
  NaN, so inputPer1k: Infinity fell through the cache-absence path — required
  tiers are only excluded from cacheGaps — and emitted no diff at all, for a
  model whose metered cost is not a number. Now a `malformed-rate` outcome,
  which also rejects negatives: a negative CACHE tier has a defined meaning
  (it resolves to inputPer1k), but a negative input or output has nothing to
  fall back to.

- The default-branch guard admitted a tag. github.ref_name omits the ref
  namespace, so a workflow_dispatch on a tag sharing master's short name
  could close or overwrite master's tracking issue using tagged code. Both
  issue steps now also require github.ref_type == 'branch'.

Live verdict unchanged for the seventh consecutive round: 16 agree, 1
expected deviation, 2 source conflicts, 7 uncorroborated, 0 malformed,
exit 0, 36/36 mappings. 60 tests, no network. Biome back to the 39-warning
master baseline.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
@c-1k
c-1k marked this pull request as ready for review August 18, 2026 12:51
@c-1k

c-1k commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Codex review — round 6, and the gate is now blocked

REVIEWED_SHA: df416db40ad597d90043980dc1295ab448f5582e
EXIT: 0   →  1 P1, 2 P2

The checker can silently lose cache-tier coverage and can classify non-finite required rates as agreement. The workflow's default-branch state guard also admits an identically named tag.

All three fixed in 99afd36.

This round took three attempts. The first died on a malformed shell command Codex generated for itself; the second was cut off mid-investigation. Neither produced a findings block or a verdict sentence, and neither was read as clean — an absent verdict is a dead review, not a passing one. Only the third attempt concluded.

The P1 is the third narrowing of the same check, and the first two were both still too coarse

  • A single shared occurrence floor is cleared by unaffected rows.
  • Per-field totals are only a lower bound, so an upstream addition creates headroom: once one row gains cache_write, a later loss on a different row keeps the total at the baseline and passes — while that row still answers from input/output and the comparison silently skips its missing tier.

Now a per-(source, model, tier) expectation, 51 entries measured against the live feeds. Mutation-tested end to end:

EXIT=2
models.dev: schema/coverage regression — kimi-k3 lost cache_write.

A named model and a named tier, where the aggregate version returned a total that still summed correctly.

Also: a non-finite required rate reported agree. rawTier rejects Infinity/NaN, so inputPer1k: Infinity fell through the cache-absence path — required tiers are only excluded from cacheGaps — and emitted no diff at all, for a model whose metered cost is not a number. Now a malformed-rate outcome. And github.ref_name omits the ref namespace, so a dispatch on a tag sharing master's short name could rewrite master's tracking issue; both issue steps now require github.ref_type == 'branch'.


⚠️ The final SHA has no Codex verdict, and that is not a pass

Round 7 against 99afd36 terminated with:

ERROR: stream disconnected before completion: You have no credits remaining.
codex: Review was interrupted. Please re-run /review and wait for it to complete.

The Codex account is out of OpenAI API credits. That is also the likely cause of the two round-6 deaths. 99afd36's changes are therefore covered by 60 local tests and every local gate, but not by an independent review. Recording that plainly rather than letting six green rounds imply a seventh.

Re-run codex review --base origin/master once credits are restored.

Where this landed

Seven rounds attempted, six concluded, 28 findings, every one accepted as real: 5 → 7 → 3 → 5 → 4 → 3.

The live verdict never moved — 16 agree, 1 expected deviation, 2 source conflicts, 7 uncorroborated, 0 malformed, exit 0, 36/36 mappings. Not one of the 28 fixes changed today's answer; each closed a path by which a future run could report clean while something was wrong. For a monitor that is the entire product.

Local gates on 99afd36: 60 drift tests, 4367 repo tests, biome at the 39-warning master baseline, typecheck clean for both the repo and the new scripts.

Two patterns worth keeping

The same defect reappeared four times, each time one abstraction higher. Conflict masking understatement (r1) → conflict masking disagreement (r2) → a rate above every source still called conflict (r4) → a malformed rate compared raw instead of effective (r5). One idea underneath all of them: a comparison that cannot conclude something must not thereby conclude that nothing is wrong.

Three times the monitor carried the very failure mode it exists to detect. A schema test comparing a constant to itself; a coverage floor derived from the map it was guarding; and a cache-coverage baseline whose totals could stay satisfied while a specific model lost a specific tier. Each read as coverage while providing none.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99afd36efb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/pricing-drift/sources.mts
Comment thread scripts/pricing-drift/compare.mts Outdated
@c-1k

c-1k commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Round 7 re-attempted — still blocked, and a live finding from the sweep

Gate: re-ran codex review --base origin/master on 99afd36 after a report that credits were restored. Still fails:

ERROR: stream disconnected before completion: You have no credits remaining.
warning: Falling back from WebSockets to HTTPS transport. stream disconnected...
Review was interrupted. Please re-run /review and wait for it to complete.
EXIT=1

It now dies in 1.3 KB within seconds, where earlier deaths produced 250–350 KB before dropping — the API rejects at connect rather than mid-stream. Diagnosed before reporting, since auth misrouting reads almost identically to a spend limit: codex is on API-key auth, this session's env also carries OPENAI_API_KEY, but the two fingerprints match — same key, no second org, balance simply at zero.

99afd36 still has no independent verdict. Six concluded rounds, 28 findings, all fixed; the final SHA is covered by 60 local tests and every local gate only.


🔴 Live billing exposure in resolveRates — measured, not theoretical

The prefix-match issue I deferred as a follow-up turns out to be live and considerably worse than the -fast case that surfaced it. Swept getModelRates against both sources:

20 uncataloged OpenAI/Anthropic models are silently underbilled, worst 16.7×.

model meters at actual underbill
gpt-4o-mini-tts 1.5 / 6 25 / 100 16.7×
gpt-5.4-pro 25 / 150 300 / 1800 12×
o3-pro 20 / 80 200 / 800 10×
gpt-4o-mini-transcribe 1.5 / 6 12.5 / 50 8.3×
o3-deep-research 20 / 80 100 / 400
gpt-4o-realtime-preview 25 / 100 50 / 200
o4-mini-deep-research 11 / 44 20 / 80 1.8×

Longest-prefix-first means any suffix that raises the price collapses onto the cheaper base. Every model above returns rateSource: "table" — indistinguishable on a receipt from an exact match, so the receipt is internally consistent, chain-verifiable, and wrong.

Models landing on FALLBACK_RATE are excluded and are fine: that path sets unknown: true and warns. The bug is specifically that these do not.

20 is a floor. The sweep only sees models published upstream as distinct ids, so claude-opus-5-fast is not counted — models.dev carries fast mode under experimental.modes.fast.cost rather than as its own id. The suffix family that first surfaced the class is invisible to the sweep that confirms it.

Fix direction: an allowlist of suffixes known to preserve pricing, everything else routed to the loud unknown fallback. A denylist would enumerate -fast, -pro, -tts, -realtime-preview, … and fail open on whatever ships next — and this class is defined by "a suffix we have not seen yet", so failing open is the one behaviour it must not have.

Not fixed in this PR. It is packages/core money-path (tier 0/1) and belongs in its own change with its own gates, not folded into a CI script. Raised here because this PR is what found it.

c-1k and others added 4 commits August 18, 2026 19:27
Seventh review round: 2 P1, 3 P2. Both P1s are the workflow believing a
clean result it did not actually receive.

- npm or tsx can exit 0 having written nothing — for instance if run.mts's
  direct-entry guard stops matching and main() never runs. The upload step
  only warns on a missing file and the failure predicate is false, so the
  clean step would CLOSE a live tracking issue on the strength of a report
  that does not exist. A non-empty report is now part of the zero-exit
  contract; its absence is re-coded to 2 rather than believed.

- The default-branch predicates proved only that the event ref was NAMED
  after the default branch, not that the checked-out SHA was still its head.
  Re-running an old scheduled run, or master advancing mid-run, could close
  or overwrite the tracking issue with a stale verdict. Both mutation steps
  now require the checked-out SHA to equal the current default-branch head,
  resolved via the API, and warn instead of mutating when it does not.

- Unit conversion rejects overflow. A FINITE upstream value can overflow to
  Infinity (1e308 * 10); the row stayed non-null and satisfied mapping
  coverage while resolveTier filtered the value back out, leaving a
  one-source model `agree` with neither required tier compared. Absence
  created by arithmetic is still absence read as agreement.

- Malformed-rate findings keep their answered sources. The branch hardcoded
  `sources: []`, and since coverage is derived from finding sources it
  invented a source-coverage breach with an empty missingMappings — a
  fabricated second failure reason attached to a real one.

- An allowlist can no longer absorb a conflicted tier. For ours 75/250
  against sources 50/200 and 50/300, input is a definitive conservative
  deviation but output straddles our rate; labelling the whole model
  `deviation-expected` claimed "ours is higher" while one source prices
  output above us. An allowlist excuses a deviation we understand, never a
  tier nobody can adjudicate.

Live verdict unchanged for the eighth consecutive round: 16 agree, 1
expected deviation, 2 source conflicts, 7 uncorroborated, 0 malformed,
exit 0, 36/36 mappings. 66 tests, no network.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
Eighth review round: 2 P1, 2 P2. One P1 is a regression the SEVENTH round's
fix introduced, which is the pattern this branch keeps producing: the guard
added to prevent one silent failure created another.

The freshness step sat after checkout/setup-node/npm ci, so it implicitly
required their success. A `npm ci` failure skipped it, the reporting step
then found no `fresh` output and skipped too — leaving a red scheduled run
and NO could-not-run issue. Round 2 established that a failure must always
be reported; round 7's guard quietly took it back. The probe is API-only, so
it now runs FIRST and with always(), needing neither the checkout nor node.

The rule that resolves the tension, stated at the site:

  Filing a failure issue does NOT require verified freshness — if freshness
  cannot be established, reporting anyway is the safe error. CLOSING an issue
  requires it positively, because closing on unknown state is how a live
  drift issue disappears. "We could not tell" must never resolve toward
  silence.

- Freshness is now sampled on BOTH sides of the comparison. One sample taken
  before the self-test and a live network fetch proves nothing about the
  moment of mutation; master can advance in that window while `fresh` still
  reads true. Closing requires both samples to match the checked-out SHA.

- Schema sentinels distinguish structural loss from absence. An absent row is
  not a schema change — the model may have left the feed, which coverage
  reports as a named missing mapping (exit 1). A row that still exists but has
  LOST `litellm_provider`, or a models.dev model whose `cost` object vanished,
  is the structure this tool pins on disappearing: exit 2, not drift.

- run.mts sets `process.exitCode` instead of calling `process.exit()`. A
  forced exit can terminate the process with stdout still buffered,
  truncating the Markdown or leaving `--json` invalid — a successful check
  reporting a corrupt result.

Live verdict unchanged for the ninth consecutive round: 16 agree, 1 expected
deviation, 2 source conflicts, 7 uncorroborated, 0 malformed, exit 0, 36/36
mappings. 70 tests, no network.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
Ninth review round: 1 P1, and findings are converging (5, 7, 3, 5, 4, 3, 5,
4, 1).

Round 8 established that CLOSING an issue requires positively verified
freshness while FILING one does not. It left the third case open: a run that
is clean AND stale. The self-test passes, the comparison passes, freshness is
false — so the close step is skipped, the failure predicate ignores freshness
entirely, and the workflow finishes GREEN having compared a tree that is not
the one master serves. The current table was never examined and nothing said
so.

That is this branch's recurring defect in its last hiding place: a state
nobody classified, resolving toward silence.

- A stale or unverified head now routes to the could-not-check body and fails
  the job, whatever the comparison returned.
- A stale run COMMENTS on an existing issue instead of editing it. Overwriting
  a live drift issue's body with "could not run" would erase findings a fresh
  run established — worse than the staleness being reported.
- The freshness state is printed in the failure step and carried in the issue
  body, so the reason is legible without reading the workflow.

Live verdict unchanged for the tenth consecutive round: 16 agree, 1 expected
deviation, 2 source conflicts, 7 uncorroborated, 0 malformed, exit 0, 36/36
mappings. 70 tests.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
…r source

Two P2s from the chatgpt-codex-connector review threads on #129 — a
different surface from the CLI gate, and one that blocks the merge until
resolved.

- Wholesale disappearance of mapped rows was the quietest possible outcome.
  Round 8 established that an ABSENT row is not a schema change, because a
  model may simply have left the feed and coverage names it. Taken to its
  limit that was wrong: if LiteLLM returns `{}` or wraps the rows in a new
  container, EVERY mapped lookup hits that same `continue`, `problems` stays
  empty, and the sentinel passes — the run then reports drift, or nothing,
  for a response it never understood. One absent row is a removal; every
  absent row is the shape having changed. Both sentinels now fail when no
  mapped row resolves at all.

- Tier differences were attributed to every source that answered the MODEL.
  When both sources publish input/output but only one publishes the cache
  tier that differs, the report listed both beside that comparison — crediting
  a rate to a source that never stated it, so a reader checking the other
  source finds nothing and cannot tell whether the tool or the source is
  wrong. TierDiff now carries `publishedBy`, resolved per tier, and the
  report renders "per litellm" or "per litellm + models.dev" against each
  difference. Model-level sources are still shown, relabelled "answered".

Live verdict unchanged for the eleventh consecutive round: 16 agree, 1
expected deviation, 2 source conflicts, 7 uncorroborated, 0 malformed,
exit 0, 36/36 mappings. 71 tests.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
@c-1k

c-1k commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Codex rounds 7–9, connector threads, and the current gate state

Round 7 — 99afd36 · EXIT 0 · 2 P1, 3 P2 → fixed in 992fd20

The workflow can accept or publish stale clean results, undermining the monitor's alerting contract.

Both P1s were the workflow believing a clean result it never received. npm/tsx can exit 0 having written no report — the upload step only warns on a missing file, the failure predicate is false, and the clean step would then close a live tracking issue on the strength of a report that does not exist. A non-empty report is now part of the zero-exit contract. Separately, the default-branch predicates proved only that the event ref was named after the default branch, not that the checked-out SHA was still its head.

Also: unit conversion rejects overflow (a finite 1e308 becomes Infinity, the row stays non-null and satisfies coverage while resolveTier filters the value back out — a one-source model reaching agree with neither required tier compared); malformed-rate findings keep their answered sources rather than fabricating a coverage breach; and an allowlist can no longer absorb a conflicted tier.

Round 8 — 992fd20 · EXIT 0 · 2 P1, 2 P2 → fixed in ee92030

One P1 was a regression round 7's own fix introduced. The freshness step sat after checkout/setup-node/npm ci, so it implicitly required their success — an npm ci failure skipped it, the reporting step then found no fresh output and skipped too, leaving a red run and no could-not-run issue. Round 2 established that failures are always reported; round 7's guard quietly took it back.

The rule that resolves it, now stated at the site:

Filing a failure issue does not require verified freshness — if freshness cannot be established, reporting anyway is the safe error. Closing requires it positively, because closing on unknown state is how a live drift issue disappears. "We could not tell" must never resolve toward silence.

Round 9 — ee92030 · EXIT 0 · 1 P1 → fixed in 819a420

Round 8 left the third case open: clean and stale. Self-test passes, comparison passes, freshness false — close step skipped, failure predicate ignoring freshness, workflow finishing green having compared a tree master no longer serves. A stale head is now a could-not-check that fails the job, and a stale run comments rather than overwriting a live drift body.

Connector threads (a separate surface) → fixed in e11fd72, both resolved

chatgpt-codex-connector posted two P2s as review threads, which blocked the merge:

  • Wholesale row disappearance was the quietest possible outcome. Round 8 established that an absent row is not a schema change. At the limit that was wrong: if LiteLLM returns {} or rewraps its rows, every mapped lookup hits that same continue, problems stays empty, and the sentinel passes. One absent row is a removal; every absent row is the shape having changed.
  • Tier differences were attributed to every source that answered the model, so a cache-tier difference published by one source was rendered beside both — crediting a rate to a source that never stated it. TierDiff now carries publishedBy, resolved per tier.

State

e11fd72 — CI green (completed, non-skipped, all 6 jobs), MERGEABLE/CLEAN, zero unresolved threads, 71 tests.

Nine concluded CLI rounds plus the connector pass: 35 findings, every one accepted as real, trending 5 → 7 → 3 → 5 → 4 → 3 → 5 → 4 → 1. The live verdict has been identical through all of them — 16 agree, 1 expected deviation, 2 source conflicts, 7 uncorroborated, 0 malformed, exit 0, 36/36 mappings.

The gate is blocked again, with a different cause than before. Round 10 against 819a420:

ERROR: stream disconnected before completion:
Your organization has reached its configured enforced spend limit.
Update your limit at https://platform.openai.com/settings/organization/limits

Note this is not the earlier You have no credits remaining — that was a zero balance, this is a configured cap, and they are different settings pages. Reading the error text rather than assuming a repeat is the difference between topping up an account that has funds and raising a limit that is throttling it.

So e11fd72 carries CI, 71 tests and a mutation-verified guard suite, but no independent verdict. Recording that rather than letting nine green rounds imply a tenth.

…ered

`HEADINGS` was already a total Record, so a new outcome could not be added
without a heading. But SECTION_ORDER — the array that actually drives
rendering — was hand-listed, and the summary table was nine hand-written
rows. Omitting an outcome from either compiles cleanly, so a new failing
outcome could be counted, could fail the run, and could appear in no table
and no section: present in the verdict, absent from the report explaining it.

Both are now derived from one compile-checked list. Exhaustiveness that
depends on remembering is a failure rate, not a guarantee.

THE GUARD ITSELF WAS VACUOUS TWICE BEFORE IT WORKED, and neither dead
version was detectable by reading it:

1. `type _Uncovered = Exclude<..., (typeof SECTION_ORDER)[number]>` with the
   array annotated `Exclude<Outcome, "agree">[]`. That annotation makes
   `[number]` the DECLARED element type — every outcome — so `_Uncovered` is
   `never` whatever the array contains. Fixed with `as const satisfies`.
2. `const x: _Uncovered[] = []`. An empty array is assignable to `T[]` for
   every T, so it passes whatever `_Uncovered` resolves to. Fixed with
   `type _AssertNever<T extends never> = T`.

Both were found by deleting a member and checking the build actually breaks.
The third form fails with `Type '"unmapped"' does not satisfy the constraint
'never'` — naming the omission — and compiles clean when whole.

A guard is not verified by being present. This one looked correct twice
while checking nothing, in the file whose subject is exactly that failure.

Live verdict unchanged: 16 agree, 1 expected deviation, 2 source conflicts,
7 uncorroborated, 0 malformed, exit 0, 36/36 mappings. 71 tests.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
@c-1k

c-1k commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Exhaustiveness made mechanical — and the guard that did it was vacuous twice

HEADINGS was already a total Record, so a new outcome could not be added without a heading. That covered less than it looked: SECTION_ORDER, the array that actually drives rendering, was hand-listed, and the summary table was nine hand-written rows. Omitting an outcome from either compiles cleanly — so a new failing outcome could be counted, could fail the run, and could appear in no table and no section. Present in the verdict, absent from the report explaining it.

Both are now derived from one compile-checked list.

The guard was dead twice, and neither version was detectable by reading it

Attempt 1type _Uncovered = Exclude<Exclude<Outcome,"agree">, (typeof SECTION_ORDER)[number]>, with the array annotated Exclude<Outcome,"agree">[]. That annotation makes [number] the declared element type — every outcome — so _Uncovered is never whatever the array contains. Fixed with as const satisfies readonly ….

Attempt 2const _check: _Uncovered[] = []. An empty array is assignable to T[] for every T, so it passes whatever _Uncovered resolves to. Fixed with type _AssertNever<T extends never> = T.

Attempt 3 fails with Type '"unmapped"' does not satisfy the constraint 'never', naming the omission, and compiles clean when whole.

Both dead versions were found by deleting a member and checking the build actually breaks — neither by re-reading. A guard is not verified by being present, and this one looked correct twice while checking nothing, in the file whose subject is precisely that failure.

The procedural fix, now standard here: run the positive control first. Prove the mutated form errors, then prove the whole form is clean. Ordered the other way I would have accepted both dead versions, because absence of a compile error reads identically to a working guard.


State

f1f93a3 — CI green on the exact SHA (completed, non-skipped, six jobs), MERGEABLE/CLEAN, zero unresolved threads, 71 tests.

Live verdict, unchanged through every round: 16 agree · 1 expected deviation · 2 source conflicts · 7 uncorroborated · 0 malformed · exit 0 · 36/36 mappings.

Two commits now sit past the last independent verdict — the connector fixes (e11fd72) and this one (f1f93a3). The Codex gate is still capped; re-probed just now rather than assumed:

enforced spend limit   (terminated ~60s, 1272 bytes)

Nine concluded CLI rounds plus the connector pass: 35 findings, every one accepted as real. Recording the two ungated commits explicitly rather than letting nine green rounds cover them.

c-1k and others added 7 commits August 18, 2026 21:07
…wn drift

Eleventh round: 2 P2, no P1.

THE COMPILE-TIME GUARD WAS NEVER EXECUTED BY CI. `scripts/` is covered by no
project in the root tsconfig and `drift:test` runs through transpile-only
tsx, so the exhaustiveness assertion added last commit fired only when
someone ran `tsc` by hand with an ad-hoc config. Adding an Outcome without
updating SECTION_ORDER would still have passed CI and silently omitted that
outcome from every report.

That is the same defect the guard exists to prevent, one level up, and I had
already reported the underlying gap — "scripts/ isn't covered by any
tsconfig" — before building a compile-only guard there anyway. The ad-hoc
config I kept creating and deleting to check it WAS the problem: a check that
only runs when its author remembers to run it has the failure rate of
remembering.

Fixed with a tracked `scripts/pricing-drift/tsconfig.json` wired into
`npm run typecheck`, which CI runs on every push and PR. Positive control
first: removing "unmapped" from SECTION_ORDER now fails `npm run typecheck`
with `Type '"unmapped"' does not satisfy the constraint 'never'`, and the
whole form is clean.

- Inconclusive runs no longer overwrite known drift. Scoping the
  comment-instead-of-edit rule to STALE heads was too narrow: a fresh run
  whose source returns 500 also reaches the edit path and replaces a live
  drift body with a generic could-not-run notice. Every inconclusive result
  now comments; only a conclusive one — instrument working, tree current,
  real report produced — has standing to replace what an earlier conclusive
  run established. Losing a real finding to a transient outage is strictly
  worse than the outage.

Live verdict unchanged: 16 agree, 1 expected deviation, 2 source conflicts,
7 uncorroborated, 0 malformed, exit 0, 36/36 mappings. 71 tests.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
Committed the previous change without reading biome's output, which had
flagged this file: spaces where the repo uses tabs. Exit status was not
checked and the summary line was not read — the same 'confirm the instrument
said what you assumed' failure this branch keeps documenting, committed by
the author documenting it.

Biome back to the 39-warning master baseline, 0 errors.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
…pload soft

Twelfth round: 3 P2, no P1.

- Cache gaps were attributed to every source that answered the MODEL. The
  round-11 fix added `publishedBy` to TierDiff and stopped there; `cacheGaps`
  was still a bare Tier[] rendered beside `f.sources`, so an omitted tier only
  one source publishes was credited to both — sending a reader to check a
  source that never stated a cache rate. Same defect, adjacent field, missed
  because the first fix was applied to the structure that had the bug rather
  than to the property that was wrong.

- A null LiteLLM row was dereferenced. `row === undefined` admits a retained
  key holding `null`, and the next property read throws — collapsing a NAMED
  missing-mapping report into a generic exit-2 "could not check". The schema
  sentinel already treated such rows as absent; normalization now agrees.

- An artifact-upload failure filed nothing. After a CLEAN comparison it
  satisfies neither the issue predicate nor the failure predicate, so the job
  ended red having recorded nothing — a scheduled failure with no trace, which
  is the exact state this workflow exists to prevent. The upload is a
  convenience and the issue is the real channel, so upload is now non-fatal.

Live verdict unchanged: 16 agree, 1 expected deviation, 2 source conflicts,
7 uncorroborated, 0 malformed, exit 0, 36/36 mappings. 73 tests.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
Thirteenth round: 1 P2.

`rawTier` preserves a negative cache entry; the canonical resolver replaces
it with `inputPer1k`. The formatter printed only the raw value, so
`cacheReadPer1k: -5` against sources at 5–10 read "ours -5" while both
classification and metering used 50 — a report contradicting its own section
heading, on the one number in it that represents money.

Differences now render as `-5 (meters at 50)` whenever the effective rate
differs from the field, matching the treatment omitted tiers already had.

Live verdict unchanged: 16 agree, 1 expected deviation, 2 source conflicts,
7 uncorroborated, 0 malformed, exit 0, 36/36 mappings. 74 tests.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
…erdict

Fourteenth round: 1 P2, and it is the sharpest test finding of the branch.

Every existing test drove `compareTable` and read its RETURN VALUE. The
workflow reads a PROCESS EXIT CODE. Nothing connected the two, so `main()`
could produce a non-empty failed report and still return 0 while all 74 tests
passed — and the weekly run would accept that zero and CLOSE a valid drift
issue. The mandatory positive control did not cover the contract the whole
alerting design rests on.

THE FIRST ATTEMPT AT THIS TEST WAS ITSELF THE DEFECT IT DESCRIBES. It fed
`{}` to both sources, which trips the SCHEMA SENTINEL and returns 2 from an
early path — never reaching the `failed ? 1 : 0` line it was written to
protect. Forcing that line to `return 0` left it passing. A test named for
one guard reaching another, in the file whose subject is exactly that.

Rebuilt with fixtures derived from the shipped MODEL_MAP and PRICING_TABLE,
so every mapped row is present and valid and the run reaches the comparison.
The baseline reproduces claude-sonnet-5's real upstream deviation rather than
flattening it — a corpus where everything agrees makes that allowlist entry
stale, which is a genuine failure and would have made the negative control
lie in the other direction.

Mutation-verified in both directions: forcing `main()` to return 0 fails
exactly `returns 1 when a rate drifts — the code the workflow actually
reads`, and the unmutated form passes 76.

Live verdict unchanged: 16 agree, 1 expected deviation, 2 source conflicts,
7 uncorroborated, 0 malformed, exit 0, 36/36 mappings.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
Committed the previous change without reading biome's summary line — the
SECOND time on this branch. Both times the tail of the output was blank or
truncated and I treated absence of a visible error as absence of an error.
'Found N errors' is the verdict; anything else is not.

Reading the instrument's own summary rather than the shape of its output is
the whole discipline this branch documents, and I have now failed it twice
while writing it down.

Biome: 0 errors, 39 warnings (master baseline).

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
The previous commit left `npm run typecheck` failing with TS18048 — and I
committed it after the check had already told me so, because I read the
absence of my own 'typecheck OK' echo rather than the command's output.

That is the THIRD instrument-reading failure on this branch and the second in
consecutive commits: blank output taken for clean output, a missing success
echo not noticed, and earlier a biome summary not read. The lesson this
branch keeps writing down is that a check's verdict must be read, not
inferred from the shape of what it printed — and CI would have caught this
one, which is precisely why local checks that are run but not read are worse
than not running them: they manufacture false confidence.

All gates now verified by reading their output: typecheck clean, biome 0
errors / 39 warnings, 76 tests, live run exit 0, and the exit-code mutation
still fails its named test.

Signed-off-by: Cam <cam@camwhiteus.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cam <cam@camwhiteus.com>
@c-1k

c-1k commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Codex gate — CLEAN on the merge SHA

codex review --base origin/master     model: gpt-5.6-sol (max effort)
REVIEWED_SHA: c84e631fb1eda4480ad760726afd5caa44414a33
EXIT: 0        187 references to the changed files

Reviewed all changed implementation/workflow files, verified applicable guidance, and ran the checker tests, TypeScript check, Biome, and actionlint. No actionable correctness defect remained.

Four independent signals agree, which is the bar this PR has been holding itself to: the process terminated (EXIT=0), the tree did not move (REVIEWED_SHA, not UNATTRIBUTABLE), the review saw the diff (187 file references, so not a no-op against an empty base), and the reviewer concluded something (an explicit verdict sentence, not an absence).

The round before this one was discarded, not read

Round 15 came back EXIT=0 with a full transcript — and UNATTRIBUTABLE: 9c34031 -> c84e631. I had edited the worktree while it was running, which codex review reads directly, so its verdict describes a tree that never existed. Discarded and relaunched against a quiescent tree rather than mining it for findings. A confident review of a mixed state is worse than no review, because it looks like evidence.

Three instrument-reading failures of my own, all in the last few commits

Worth recording, because they are the same class the tool is built to catch and I committed them while writing it down:

  • committed with a Biome error unread — the output tail was blank and I treated that as clean;
  • committed with npm run typecheck failing — I watched for my own typecheck OK echo and did not notice its absence;
  • ran a review over a tree I was actively editing.

The first two were caught by re-running and reading the summary line; the third by the REVIEWED_SHA guard. Found N errors is the verdict — not the shape of the output, and not the absence of something red.

Also worth noting: my own verdict extractor returned nothing here. It keys on Full review comments / Review comment:, which a clean run never prints — so a clean review and a dead review looked identical to it, and only reading the raw tail distinguished them. Exactly the "an extractor keying on one marker form has the same bug as the grep it replaced" problem, now with a third marker form.


Final state

c84e631 — 19 commits · Codex CLEAN · CI green on the exact SHA (6 jobs, completed, non-skipped) · MERGEABLE/CLEAN · zero unresolved threads · 76 tests.

Live verdict, identical through every one of the fifteen rounds: 16 agree · 1 expected deviation · 2 source conflicts · 7 uncorroborated · 0 malformed · exit 0 · 36/36 mappings.

Fourteen concluded CLI rounds plus a connector pass: 41 findings, every one accepted as real, trending 5 → 7 → 3 → 5 → 4 → 3 → 5 → 4 → 1 → [connector 2] → 2 → 3 → 1 → 1 → 0.

Not one of the 41 fixes changed today's answer. Each closed a path by which a future run could report clean while something was wrong — which, for a monitor, is the entire product.

What the rounds were actually about

One idea kept resurfacing one abstraction higher each time: a comparison that cannot conclude something must not thereby conclude that nothing is wrong. Conflict masking understatement → conflict masking disagreement → a rate above every source still called a conflict → a malformed rate compared raw instead of effective → a clean-but-stale run finishing green.

And five times the monitor carried, in its own instrument, the failure it exists to detect: a schema test comparing a constant to itself; a coverage floor derived from the map it guarded; a cache baseline whose totals stayed satisfied while one model lost one tier; an exhaustiveness guard that was vacuous twice; and a positive control that never exercised the exit code the workflow actually reads.

Every guard in this PR has been mutation-verified in both directions — including against the live feeds — because a guard is not verified by being present.

The monitor counted `corroboration = 2` when LiteLLM and models.dev agreed
and treated that as independent confirmation. They are not independent: both
derive from the providers' published pages and both lag promotional changes,
so N of them agreeing is ONE upstream fact with N mirrors. The strongest
signal was systematically overstated in exactly the case where verification
matters most — a rate that just moved.

Measured 2026-08-22: both catalogs, and this repo's own table, priced
`gpt-5.6-sol` at 50/300. OpenAI's page said 40/200, footnoted promotional
through 2026-11-21. Acting on "three-way agreement" would have overcharged
output by 50% on the most-used model, with three sources apparently
confirming it.

CONFIRMED BY EVENTS WITHIN HOURS. LiteLLM has since moved to 40/200 while
models.dev still carries 50/300, so the two now visibly disagree on that
exact rate and the monitor reports it as a source conflict. The "agreement"
was a TIMING ARTEFACT — two mirrors of one page, one of which had not
refreshed. That also means this repo's own table is stale at 50/300, caught
by its own checker.

Adds explicit precedence — billed > provider > catalog — and reports
`evidence`, the count of distinct evidence TIERS, rather than a source count.
Two catalogs agreeing is 1. A catalog agreeing with a provider page is 2. The
report now reads "litellm, models.dev — 1 evidence tier" instead of implying
two confirmations.

The narrower lesson, which is the durable one: before calling a
provider-vs-catalog disagreement an error, ask WHICH QUESTION EACH SIDE IS
ANSWERING. List vs promotional, standard vs priority tier, base vs
context-cliff, 5m vs 1h cache-write are different questions, and a catalog
silently answers whichever one it was built from.

Outcome-neutral: verified by running HEAD without this change against the
same live feeds and getting an identical verdict.

Tests 78, typecheck clean.

Signed-off-by: Cam <cam@camwhiteus.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant