From 61e76b798930756b83d45d73418f2b3c02f146b4 Mon Sep 17 00:00:00 2001 From: Aditya Vikram Singh <247195684+avs-io@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:56:25 +0530 Subject: [PATCH] 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. --- scripts/upgrade-path/run.mjs | 2 +- src/codex-credits.ts | 11 ++++++++++- src/daily-cache.ts | 22 +++++++++++++--------- src/models.ts | 13 +++++++------ tests/codex-credits.test.ts | 10 ++++++++++ tests/models.test.ts | 10 +++++----- tests/providers/codex.test.ts | 6 +++--- 7 files changed, 49 insertions(+), 25 deletions(-) diff --git a/scripts/upgrade-path/run.mjs b/scripts/upgrade-path/run.mjs index f4bb0e53..8e15392c 100644 --- a/scripts/upgrade-path/run.mjs +++ b/scripts/upgrade-path/run.mjs @@ -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') diff --git a/src/codex-credits.ts b/src/codex-credits.ts index 10d5b1f4..0e848d9e 100644 --- a/src/codex-credits.ts +++ b/src/codex-credits.ts @@ -20,10 +20,19 @@ const CREDITS_PER_MILLION: Record = { '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 = { + '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']! diff --git a/src/daily-cache.ts b/src/daily-cache.ts index 8b2eb80d..f56e3bdd 100644 --- a/src/daily-cache.ts +++ b/src/daily-cache.ts @@ -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 diff --git a/src/models.ts b/src/models.ts index 708b4e6e..2c92354a 100644 --- a/src/models.ts +++ b/src/models.ts @@ -283,14 +283,15 @@ const BUILTIN_ALIASES: Record = { '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', diff --git a/tests/codex-credits.test.ts b/tests/codex-credits.test.ts index cfa6c482..29147dc7 100644 --- a/tests/codex-credits.test.ts +++ b/tests/codex-credits.test.ts @@ -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) + }) }) diff --git a/tests/models.test.ts b/tests/models.test.ts index 5dd71f91..f6e49cd8 100644 --- a/tests/models.test.ts +++ b/tests/models.test.ts @@ -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', () => { diff --git a/tests/providers/codex.test.ts b/tests/providers/codex.test.ts index 391b6d8a..1da3a16e 100644 --- a/tests/providers/codex.test.ts +++ b/tests/providers/codex.test.ts @@ -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']