mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-25 16:33:14 +00:00
Merge pull request #1056 from avs-io/fix/1047-codex-activity-ids
fix(models): price Codex activity ids via the official underlying model
This commit is contained in:
commit
13c1785df7
9 changed files with 156 additions and 11 deletions
|
|
@ -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.v24.json'
|
||||
const NEW_DAILY_CACHE = 'daily-cache.v25.json'
|
||||
|
||||
const HOME = join(WORK, 'user home')
|
||||
const PAYLOADS = join(WORK, 'payloads')
|
||||
|
|
|
|||
|
|
@ -40,7 +40,10 @@ import type { ParsedProviderCall } from './providers/types.js'
|
|||
// in `skills`. This file stores each call's `tools`/`toolSequence`/`skills`
|
||||
// verbatim (they are passed through on read, never re-derived), so v13 entries
|
||||
// keep the old, MCP- and skill-less attribution until they re-parse.
|
||||
export const CODEX_CACHE_VERSION = 14
|
||||
// v15: builtin alias prices `codex-auto-review` (#1047). Exact-hit cache
|
||||
// entries still hold the pre-alias $0; bump so unchanged rollouts reprice.
|
||||
// Must be max(main v14 #1092, this)+1 — #1092 spent v14 on MCP/skills.
|
||||
export const CODEX_CACHE_VERSION = 15
|
||||
export const CODEX_LEGACY_CACHE_FILE = 'codex-results.json'
|
||||
export function codexCacheFileName(version = CODEX_CACHE_VERSION): string {
|
||||
return `codex-results.v${version}.json`
|
||||
|
|
|
|||
|
|
@ -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']!
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@ import { join } from 'path'
|
|||
import { getCodeburnCacheDir } from './cache-dir.js'
|
||||
import type { DateRange, ProjectSummary } from './types.js'
|
||||
|
||||
// Bumped to 25: `codex-auto-review` now prices as the recommended GPT-5.5
|
||||
// row (#1047). Days already finalized under v24 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. v21 is
|
||||
// #946, v22 was this PR's earlier claim, v23 is #1075, v24 is #1090.
|
||||
//
|
||||
// Bumped to 20: the Codex fast-path read a nested
|
||||
// `base_instructions.provenance.model` out of `session_meta` as if it were
|
||||
// `payload.model` (#1040), so every call a rollout attributed from session
|
||||
|
|
@ -128,11 +137,10 @@ import type { DateRange, ProjectSummary } from './types.js'
|
|||
// window where neither existed). The daily cache has no per-provider
|
||||
// invalidation, so there is no way to tell those days apart from here -
|
||||
// raising MIN_SUPPORTED_VERSION forces the one-time re-derivation for
|
||||
// everyone, which is a lossless no-op for days already correct. #1056 also
|
||||
// claims 24 on its own branch (unmerged as of this writing); whichever lands
|
||||
// second takes the next number instead of reusing this one.
|
||||
export const DAILY_CACHE_VERSION = 24
|
||||
const MIN_SUPPORTED_VERSION = 24
|
||||
// everyone, which is a lossless no-op for days already correct.
|
||||
// v25: #1047 activity-id pricing. v24 on main already shipped #1090.
|
||||
export const DAILY_CACHE_VERSION = 25
|
||||
const MIN_SUPPORTED_VERSION = 25
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -312,6 +312,16 @@ const BUILTIN_ALIASES: Record<string, string> = {
|
|||
'openclaw-auto': 'claude-sonnet-4-5',
|
||||
'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. 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.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',
|
||||
|
|
@ -1017,6 +1027,7 @@ const autoModelNames: Record<string, string> = {
|
|||
'openclaw-auto': 'OpenClaw (auto)',
|
||||
'qwen-auto': 'Qwen (auto)',
|
||||
'kimi-auto': 'Kimi (auto)',
|
||||
'codex-auto-review': 'Codex Auto Review',
|
||||
}
|
||||
|
||||
const SHORT_NAMES: Record<string, string> = {
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ export const PROVIDER_PARSE_VERSIONS: Record<string, string> = {
|
|||
// session-meta-model-v1: parse large session_meta records structurally so a
|
||||
// nested base_instructions provenance.model cannot overwrite turn_context.
|
||||
// session-meta-fields-v1: the same depth-1 window for cwd/name/originator/
|
||||
// session_id/forked_from_id/model_provider, not just model.
|
||||
// session_id/forked_from_id/model_provider, not just model. (#1055)
|
||||
// codex-pricing-v1 (#1075): reasoning tokens are no longer added on top of
|
||||
// output, and cache_write_input_tokens moves out of the plain input bucket on
|
||||
// models with an explicit cache-write rate. The bucket move does NOT self-heal
|
||||
|
|
@ -294,7 +294,10 @@ export const PROVIDER_PARSE_VERSIONS: Record<string, string> = {
|
|||
// through the `exec` custom tool or the item model's `CommandExecution` item
|
||||
// were counted as Bash only. Cached sessions store tools/toolSequence/skills
|
||||
// verbatim, so they must re-parse to gain the attribution.
|
||||
codex: 'mcp-attribution-v5-est-cost-active-timing-mcp-wait-rich-capture-v1-cross-provider-pr-v1-session-meta-model-v1-session-meta-fields-v1-codex-pricing-v1-codex-tps-v1-codex-mcp-skills-v1',
|
||||
// activity-price-v1: `codex-auto-review` now prices via the recommended
|
||||
// review model. session-cache.json would otherwise keep the pre-alias $0.
|
||||
// Compose all four — a take-ours merge would drop #1075, #1079, or #1092.
|
||||
codex: 'mcp-attribution-v5-est-cost-active-timing-mcp-wait-rich-capture-v1-cross-provider-pr-v1-session-meta-model-v1-session-meta-fields-v1-codex-pricing-v1-codex-tps-v1-codex-mcp-skills-v1-activity-price-v1',
|
||||
cursor: 'composer-anchored-crediting-v1-est-cost',
|
||||
'cursor-agent': 'workspaceless-transcript-v1',
|
||||
// source-provenance-v1 (#944): CLI sessions were misread as VS Code
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
@ -45,4 +51,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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -653,6 +653,8 @@ describe('Cursor model variants resolve to pricing', () => {
|
|||
['claude-4.6-haiku', 'claude-haiku-4-5'],
|
||||
// Cursor auto proxy
|
||||
['cursor-auto', 'claude-sonnet-4-5'],
|
||||
// Codex activity surface (official rate card, observed raw id)
|
||||
['codex-auto-review', 'gpt-5.5'],
|
||||
// OpenAI variants Cursor emits
|
||||
['gpt-5', 'gpt-5'],
|
||||
['gpt-5-fast', 'gpt-5'],
|
||||
|
|
@ -688,6 +690,29 @@ describe('Cursor model variants resolve to pricing', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Codex activity ids (#1047)', () => {
|
||||
it('keeps the activity label instead of collapsing to the underlying model name', () => {
|
||||
expect(getShortModelName('codex-auto-review')).toBe('Codex Auto Review')
|
||||
})
|
||||
|
||||
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 gpt55 = calculateCost('gpt-5.5', 1_000_000, 1_000_000, 0, 0, 0)
|
||||
expect(auto).toBeGreaterThan(0)
|
||||
expect(auto).toBe(gpt55)
|
||||
})
|
||||
|
||||
it('does not invent a family or an unobserved sibling id', () => {
|
||||
expect(getModelCosts('codex-code-review')).toBeNull()
|
||||
expect(getModelCosts('codex-cloud-task')).toBeNull()
|
||||
expect(getModelCosts('codex-automation')).toBeNull()
|
||||
expect(getModelCosts('code-review')).toBeNull()
|
||||
expect(getModelCosts('auto-review')).toBeNull()
|
||||
expect(calculateCost('codex-code-review', 1_000_000, 1_000_000, 0, 0, 0)).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cursor house model pricing', () => {
|
||||
const cases: Array<[string, { input: number; output: number; cacheWrite: number; cacheRead: number }]> = [
|
||||
['composer-2.5', { input: 0.5, output: 2.5, cacheWrite: 0.5, cacheRead: 0.2 }],
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { mkdtemp, mkdir, writeFile, rm } from 'fs/promises'
|
||||
import { mkdtemp, mkdir, writeFile, rm, stat } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
import { createCodexProvider } from '../../src/providers/codex.js'
|
||||
import { clearCodexMemCaches, CODEX_CACHE_VERSION, codexCacheFileName } from '../../src/codex-cache.js'
|
||||
import { calculateCost } from '../../src/models.js'
|
||||
import type { ParsedProviderCall } from '../../src/providers/types.js'
|
||||
|
||||
let tmpDir: string
|
||||
|
|
@ -1317,3 +1319,77 @@ describe('codex provider - forked session dedupe', () => {
|
|||
expect(tokens).toBe(300)
|
||||
})
|
||||
})
|
||||
|
||||
describe('codex auto-review pricing (#1047)', () => {
|
||||
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'),
|
||||
tokenCount({
|
||||
timestamp: '2026-04-14T10:01:00Z',
|
||||
last: { input: 1_000_000, output: 1_000_000 },
|
||||
total: { total: 2_000_000 },
|
||||
}),
|
||||
])
|
||||
const provider = createCodexProvider(tmpDir)
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of provider.createSessionParser({ path: filePath, project: 'test', provider: 'codex' }, new Set()).parse()) {
|
||||
calls.push(call)
|
||||
}
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.model).toBe('codex-auto-review')
|
||||
expect(calls[0]!.costUSD).toBe(calculateCost('gpt-5.5', 1_000_000, 1_000_000, 0, 0, 0))
|
||||
})
|
||||
|
||||
it('discards a warm v11 versioned $0 exact hit so unchanged rollouts reprice', async () => {
|
||||
const cacheDir = join(tmpDir, 'cache')
|
||||
await mkdir(cacheDir, { recursive: true })
|
||||
const prev = process.env['CODEBURN_CACHE_DIR']
|
||||
process.env['CODEBURN_CACHE_DIR'] = cacheDir
|
||||
try {
|
||||
const filePath = await writeSession(tmpDir, '2026-04-14', 'rollout-stale-auto.jsonl', [
|
||||
sessionMeta({ session_id: 'sess-stale-auto', model: 'codex-auto-review' }),
|
||||
userMessage('review the PR'),
|
||||
tokenCount({
|
||||
timestamp: '2026-04-14T10:01:00Z',
|
||||
last: { input: 1_000_000, output: 1_000_000 },
|
||||
total: { total: 2_000_000 },
|
||||
}),
|
||||
])
|
||||
const st = await stat(filePath)
|
||||
// Main's #1075 already owns v11. A colliding v11 $0 file must not be
|
||||
// treated as current after this PR takes v12.
|
||||
expect(CODEX_CACHE_VERSION).toBeGreaterThan(11)
|
||||
await writeFile(join(cacheDir, codexCacheFileName(11)), JSON.stringify({
|
||||
version: 11,
|
||||
files: {
|
||||
[filePath]: {
|
||||
mtimeMs: st.mtimeMs,
|
||||
sizeBytes: st.size,
|
||||
project: 'test',
|
||||
calls: [{
|
||||
model: 'codex-auto-review',
|
||||
costUSD: 0,
|
||||
inputTokens: 1_000_000,
|
||||
outputTokens: 1_000_000,
|
||||
deduplicationKey: 'stale',
|
||||
}],
|
||||
},
|
||||
},
|
||||
}))
|
||||
clearCodexMemCaches()
|
||||
const provider = createCodexProvider(tmpDir)
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of provider.createSessionParser({ path: filePath, project: 'test', provider: 'codex' }, new Set()).parse()) {
|
||||
calls.push(call)
|
||||
}
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.costUSD).toBeGreaterThan(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']
|
||||
else process.env['CODEBURN_CACHE_DIR'] = prev
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue