fix(codex): stop double-billing reasoning output, price cache writes at the explicit rate only

Reasoning tokens are a subset of output_tokens for OpenAI models, not an
extra bucket: on a 1,396-rollout corpus all 134,316 token_count events
carrying a total satisfy input + output == total. CodeBurn added
reasoning_output_tokens on top when pricing a codex call, in the
cache-rehydration re-price, and in the models/audit display sums. That
overstated codex cost by $166.03 (3.5%) and displayed output tokens by
34.6% on that corpus. Both cost sites and the display sums now go through
one shared billableOutputTokens() so a cold parse and a warm read cannot
drift apart.

cache_write_input_tokens (codex PR #33454) was never read and
cacheCreationInputTokens was hardcoded to 0. It is now carved out of the
uncached-input bucket and clamped to it, but routed to the cache-write
bucket ONLY when the pricing source publishes an explicit cache-write rate.
buildCosts fabricates 1.25x input when a source omits one, which is correct
for Anthropic and would have invented a surcharge OpenAI never charged on
gpt-5.5 / 5.4 / 5.3-codex / gpt-5. ModelCosts now carries
cacheWriteCostIsExplicit so that distinction survives getModelCosts.

A cost change invalidates persisted output: codex-results.json v10 -> v11
(stores costUSD verbatim), the codex parse version moves (the token-bucket
change does not self-heal on read), and the daily cache goes 20 -> 23 (21 is
claimed by the #946 landing branch and 22 by PR #1056). The upgrade-path
corpus asserts codex tokens and calls exactly and reports the repricing.

Closes #1075
This commit is contained in:
iamtoruk 2026-08-21 11:50:23 -07:00
parent e12fb39e3f
commit fda7e8024d
15 changed files with 542 additions and 27 deletions

View file

@ -22,8 +22,15 @@
// which is the part the corpus can honestly establish.
// dsh did not exist in the published CLI. Reported; required to be absent
// in the baseline and present after the upgrade.
// codex PRICING changed by design in #1075: reasoning tokens are billed
// inside output rather than on top of it, and cache writes are carved out
// of the input bucket. Nothing about what was PARSED moved, so codex keeps
// the full exact treatment for the call count and every token field; only
// the cost tolerance is lifted, and the delta is reported instead. Drop it
// from this list once a published CLI carries the fix.
const EXACT = ['claude', 'codex', 'gemini', 'kiro', 'cursor']
const CHANGED_BY_DESIGN = ['grok']
const COST_CHANGED_BY_DESIGN = ['codex']
const NEW_IN_THIS_RELEASE = ['dsh']
const COST_TOLERANCE = 0.005 // 0.5% relative
@ -95,7 +102,11 @@ for (const name of providers) {
if (b.calls !== u.calls) diffs.push(`calls ${b.calls} != ${u.calls}`)
for (const f of TOKEN_FIELDS) if (b[f] !== u[f]) diffs.push(`${f} ${b[f]} != ${u[f]}`)
const costDrift = relDiff(b.cost, u.cost)
if (costDrift > COST_TOLERANCE) diffs.push(`cost ${fmt(b.cost)} != ${fmt(u.cost)} (${(costDrift * 100).toFixed(3)}% > ${(COST_TOLERANCE * 100).toFixed(1)}%)`)
if (COST_CHANGED_BY_DESIGN.includes(name)) {
notes.push(`${name}: cost ${fmt(b.cost)} -> ${fmt(u.cost)} (${(costDrift * 100).toFixed(3)}%) — repricing expected (#1075); tokens and calls still asserted exactly`)
} else if (costDrift > COST_TOLERANCE) {
diffs.push(`cost ${fmt(b.cost)} != ${fmt(u.cost)} (${(costDrift * 100).toFixed(3)}% > ${(COST_TOLERANCE * 100).toFixed(1)}%)`)
}
if (!EXACT.includes(name)) {
notes.push(`${name}: no expectation declared in compare.mjs; ${diffs.length ? diffs.join(', ') : 'identical'}`)
verdict = diffs.length ? 'differs (unclassified)' : 'identical'

View file

@ -33,7 +33,7 @@ const WORK = process.env['UPGRADE_PATH_WORK'] || join(tmpdir(), 'codeburn upgrad
const OLD_SESSION_CACHE = 'session-cache.v7.json'
const OLD_DAILY_CACHE = 'daily-cache.v17.json'
const NEW_SESSION_CACHE_DIR = 'session-cache.v9'
const NEW_DAILY_CACHE = 'daily-cache.v20.json'
const NEW_DAILY_CACHE = 'daily-cache.v23.json'
const HOME = join(WORK, 'user home')
const PAYLOADS = join(WORK, 'payloads')