fix(models): price Codex auto-review as GPT-5.5 and take daily v22

Maintainer review on #1056: pin the activity id to the recommended
review model (existing bundled row, not an invented rate), resolve
the same alias for credits, and take daily-cache 22 so this PR does
not collide with #946 claiming v21.
This commit is contained in:
Aditya Vikram Singh 2026-08-21 15:56:25 +05:30
parent 4d15b05d84
commit 61e76b7989
7 changed files with 49 additions and 25 deletions

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.v21.json'
const NEW_DAILY_CACHE = 'daily-cache.v22.json'
const HOME = join(WORK, 'user home')
const PAYLOADS = join(WORK, 'payloads')

View file

@ -20,10 +20,19 @@ const CREDITS_PER_MILLION: Record<string, CodexCreditRate> = {
'gpt-5.4-mini': { input: 18.75, cachedInput: 1.875, output: 113 },
}
// Activity surfaces keep their product id on the call (display stays
// "Codex Auto Review"). Credits must follow the same underlying model
// BUILTIN_ALIASES uses for USD. Keep this table in lockstep with
// `codex-auto-review` in src/models.ts.
const ACTIVITY_CREDIT_MODELS: Record<string, string> = {
'codex-auto-review': 'gpt-5.5',
}
/// Resolve the credit rate for a Codex model name, tolerating suffix variants
/// (e.g. "gpt-5.5-codex"). Returns null when the model has no known credit rate.
export function codexCreditRate(model: string): CodexCreditRate | null {
const m = model.toLowerCase()
const mapped = ACTIVITY_CREDIT_MODELS[model] ?? ACTIVITY_CREDIT_MODELS[model.toLowerCase()]
const m = (mapped ?? model).toLowerCase()
if (m.includes('5.4') && m.includes('mini')) return CREDITS_PER_MILLION['gpt-5.4-mini']!
if (m.includes('5.4')) return CREDITS_PER_MILLION['gpt-5.4']!
if (m.includes('5.5')) return CREDITS_PER_MILLION['gpt-5.5']!

View file

@ -6,13 +6,17 @@ import { join } from 'path'
import { getCodeburnCacheDir } from './cache-dir.js'
import type { DateRange, ProjectSummary } from './types.js'
// Bumped to 21: `codex-auto-review` now prices as the official GPT-5.4 row
// (#1047). Days already finalized under v20 keep that id at $0 forever unless
// MIN_SUPPORTED_VERSION moves: the daily cache has no per-provider
// invalidation. The Codex parse version and CODEX_CACHE_VERSION move with
// this so the lower caches reprice first; this pass then re-derives ALL days
// from the warm session cache (seconds, not a full re-parse). adoptOlderDailyCaches
// keeps the superseded v20 file as the baseline.
// Bumped to 22: `codex-auto-review` now prices as the recommended GPT-5.5
// row (#1047). Days already finalized under v20/v21 keep that id at $0
// forever unless MIN_SUPPORTED_VERSION moves: the daily cache has no
// per-provider invalidation. The Codex parse version and CODEX_CACHE_VERSION
// move with this so the lower caches reprice first; this pass then re-derives
// ALL days from the warm session cache (seconds, not a full re-parse).
// adoptOlderDailyCaches keeps the superseded file as the baseline. v22 not
// v21: #946 (in landing) already claims 21.
//
// Bumped to 21: unreleased on this branch; skipped so we do not collide
// with #946's daily-cache.v21.json.
//
// Bumped to 20: the Codex fast-path read a nested
// `base_instructions.provenance.model` out of `session_meta` as if it were
@ -118,8 +122,8 @@ import type { DateRange, ProjectSummary } from './types.js'
// that older binaries skipped. v8 added local-model savings to the daily
// rollup; the `savingsConfigHash` field is invalidated separately when the
// user changes their `localModelSavings` mapping.
export const DAILY_CACHE_VERSION = 21
const MIN_SUPPORTED_VERSION = 21
export const DAILY_CACHE_VERSION = 22
const MIN_SUPPORTED_VERSION = 22
// Version-suffixed so different binaries each own a distinct file and never
// clobber an incompatible schema. Bumping the version mints a fresh filename;
// adoptOlderDailyCaches then unions days out of every previous file (including

View file

@ -283,14 +283,15 @@ const BUILTIN_ALIASES: Record<string, string> = {
'warp-auto-efficient': 'gpt-5.3-codex',
'warp-auto-powerful': 'claude-opus-4-6',
// Codex activity ids are product surfaces, not subscription SKUs and not
// LiteLLM rows. Official rate card (help.openai.com/articles/20001106,
// 2026-08-20): "Auto review uses GPT-5.4." Price as that existing row.
// Do not invent a rate. Do not treat the id as honestly $0 — it consumes
// the same credit pool as other Codex work. Display stays on
// autoModelNames (same class as cursor-auto / copilot-openai-auto).
// LiteLLM rows. OpenAI's tracker (openai/codex#32224) says auto review
// consumes normal model usage. Public evidence: review_model defaults to
// the session model; GPT-5.5 is the currently recommended review model.
// Price as that existing bundled row. Do not invent a rate. Do not treat
// the id as honestly $0 — it draws from the same credit pool. Display
// stays on autoModelNames (same class as cursor-auto / copilot-openai-auto).
// Only alias ids observed in Codex source / real rollouts. Do not infer
// `codex-code-review` from the activity name "code review".
'codex-auto-review': 'gpt-5.4',
'codex-auto-review': 'gpt-5.5',
'grok-build': 'grok-build-0.1',
'GPT-5.3 Codex (low reasoning)': 'gpt-5.3-codex',
'GPT-5.3 Codex (medium reasoning)': 'gpt-5.3-codex',

View file

@ -17,6 +17,12 @@ describe('codexCreditRate', () => {
expect(codexCreditRate('gpt-4o')).toBeNull()
expect(codexCreditRate('claude-opus-4-8')).toBeNull()
})
it('resolves the auto-review activity id to the same rate as GPT-5.5', () => {
expect(codexCreditRate('codex-auto-review')).toEqual(codexCreditRate('gpt-5.5'))
expect(codexCreditRate('codex-auto-review')).not.toBeNull()
expect(codexCreditRate('CODEX-AUTO-REVIEW')).toEqual(codexCreditRate('gpt-5.5'))
})
})
describe('codexCredits', () => {
@ -50,4 +56,8 @@ describe('codexCredits', () => {
it('returns null for an unknown model', () => {
expect(codexCredits('gpt-4o', { inputTokens: 1_000_000, cachedReadTokens: 0, outputTokens: 0 })).toBeNull()
})
it('charges auto-review at the GPT-5.5 credit rate, not null', () => {
expect(codexCredits('codex-auto-review', { inputTokens: 1_000_000, cachedReadTokens: 0, outputTokens: 0 })).toBe(125)
})
})

View file

@ -610,7 +610,7 @@ describe('Cursor model variants resolve to pricing', () => {
// Cursor auto proxy
['cursor-auto', 'claude-sonnet-4-5'],
// Codex activity surface (official rate card, observed raw id)
['codex-auto-review', 'gpt-5.4'],
['codex-auto-review', 'gpt-5.5'],
// OpenAI variants Cursor emits
['gpt-5', 'gpt-5'],
['gpt-5-fast', 'gpt-5'],
@ -651,12 +651,12 @@ describe('Codex activity ids (#1047)', () => {
expect(getShortModelName('codex-auto-review')).toBe('Codex Auto Review')
})
it('prices as the exact bundled GPT-5.4 object, not an invented rate', () => {
expect(getModelCosts('codex-auto-review')).toBe(getModelCosts('gpt-5.4'))
it('prices as the exact bundled GPT-5.5 object, not an invented rate', () => {
expect(getModelCosts('codex-auto-review')).toBe(getModelCosts('gpt-5.5'))
const auto = calculateCost('codex-auto-review', 1_000_000, 1_000_000, 0, 0, 0)
const gpt54 = calculateCost('gpt-5.4', 1_000_000, 1_000_000, 0, 0, 0)
const gpt55 = calculateCost('gpt-5.5', 1_000_000, 1_000_000, 0, 0, 0)
expect(auto).toBeGreaterThan(0)
expect(auto).toBe(gpt54)
expect(auto).toBe(gpt55)
})
it('does not invent a family or an unobserved sibling id', () => {

View file

@ -1201,7 +1201,7 @@ describe('codex provider - forked session dedupe', () => {
})
describe('codex auto-review pricing (#1047)', () => {
it('parses auto-review as itself and prices it as GPT-5.4', async () => {
it('parses auto-review as itself and prices it as GPT-5.5', async () => {
const filePath = await writeSession(tmpDir, '2026-04-14', 'rollout-auto-review.jsonl', [
sessionMeta({ session_id: 'sess-auto', model: 'codex-auto-review' }),
userMessage('review the PR'),
@ -1218,7 +1218,7 @@ describe('codex auto-review pricing (#1047)', () => {
}
expect(calls).toHaveLength(1)
expect(calls[0]!.model).toBe('codex-auto-review')
expect(calls[0]!.costUSD).toBe(calculateCost('gpt-5.4', 1_000_000, 1_000_000, 0, 0, 0))
expect(calls[0]!.costUSD).toBe(calculateCost('gpt-5.5', 1_000_000, 1_000_000, 0, 0, 0))
})
it('discards a warm v9 $0 exact hit so unchanged rollouts reprice', async () => {
@ -1262,7 +1262,7 @@ describe('codex auto-review pricing (#1047)', () => {
}
expect(calls).toHaveLength(1)
expect(calls[0]!.costUSD).toBeGreaterThan(0)
expect(calls[0]!.costUSD).toBe(calculateCost('gpt-5.4', 1_000_000, 1_000_000, 0, 0, 0))
expect(calls[0]!.costUSD).toBe(calculateCost('gpt-5.5', 1_000_000, 1_000_000, 0, 0, 0))
} finally {
clearCodexMemCaches()
if (prev === undefined) delete process.env['CODEBURN_CACHE_DIR']