fix(usage): price the bare gpt-5.6 alias and assert Opus 4.5 separately

OpenAI routes the bare `gpt-5.6` alias to Sol, but only the explicit
`-sol` / `-terra` / `-luna` IDs resolved, so alias-recorded sessions still
reported no cost. Match it exactly rather than by prefix so it cannot
swallow the tier IDs or a future cheaper variant.

Also split the Claude 5 shadowing guard into per-model breakdown
assertions and add the missing Opus 4.5 fixture the test name claimed.
This commit is contained in:
Brennan Benson
2026-07-26 19:07:38 -07:00
parent b4f0cdeba1
commit 3e7eac75fd
3 changed files with 56 additions and 18 deletions
+25 -18
View File
@@ -343,27 +343,34 @@ describe('ClaudeUsageStore', () => {
it('does not collapse Opus 4.5 or Sonnet 4.5 usage into Claude 5 pricing', async () => {
const store = createStoreWithState({
dailyAggregates: [
{
day: '2026-04-09',
model: 'claude-sonnet-4-5-20250929',
projectKey: 'worktree:repo-1::/workspace/repo-a',
projectLabel: 'Repo A',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo-a',
turnCount: 1,
zeroCacheReadTurnCount: 0,
inputTokens: 300_000,
outputTokens: 300_000,
cacheReadTokens: 300_000,
cacheWriteTokens: 300_000
}
]
dailyAggregates: ['claude-sonnet-4-5-20250929', 'claude-opus-4-5-20251101'].map((model) => ({
day: '2026-04-09',
model,
projectKey: 'worktree:repo-1::/workspace/repo-a',
projectLabel: 'Repo A',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo-a',
turnCount: 1,
zeroCacheReadTurnCount: 0,
inputTokens: 300_000,
outputTokens: 300_000,
cacheReadTokens: 300_000,
cacheWriteTokens: 300_000
}))
})
const summary = await store.getSummary('orca', '30d')
const breakdown = await store.getBreakdown('orca', '30d', 'model')
expect(summary.estimatedCostUsd).toBeCloseTo(8.07)
// Why: the 4.5 tier premium only survives if `-4-5-` never matches the `-5`
// family regex, so this doubles as the digit-boundary proof for both families.
expect(
breakdown.find((row) => row.key === 'claude-sonnet-4-5-20250929')?.estimatedCostUsd
).toBeCloseTo(8.07)
// Why: Opus 4.5 and Opus 5 share rates today, so this pins the rate rather
// than the routing — it fails only if the two ever diverge.
expect(
breakdown.find((row) => row.key === 'claude-opus-4-5-20251101')?.estimatedCostUsd
).toBeCloseTo(11.025)
})
it('prices unknown newer Opus 4 point releases with current Opus rates', async () => {
+25
View File
@@ -425,6 +425,31 @@ describe('CodexUsageStore', () => {
).toBeCloseTo(0.205)
})
it('prices the bare gpt-5.6 alias at Sol rates without shadowing the tier IDs', async () => {
const store = createStoreWithState({
dailyAggregates: ['gpt-5.6', 'gpt-5.6-luna'].map((model) => ({
day: '2026-04-09',
model,
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 100_000,
cachedInputTokens: 50_000,
outputTokens: 25_000,
reasoningOutputTokens: 5_000,
totalTokens: 125_000,
hasInferredPricing: false
}))
})
const breakdown = await store.getBreakdown('orca', '30d', 'model')
expect(breakdown.find((row) => row.key === 'gpt-5.6')?.estimatedCostUsd).toBeCloseTo(1.025)
expect(breakdown.find((row) => row.key === 'gpt-5.6-luna')?.estimatedCostUsd).toBeCloseTo(0.205)
})
it('normalizes Codex model variants and reasoning suffixes before pricing', async () => {
const store = createStoreWithState({
dailyAggregates: [
+6
View File
@@ -261,6 +261,12 @@ function normalizeModelForPricing(model: string | null): string | null {
if (normalized === 'gpt-5.6-luna' || normalized.startsWith('gpt-5.6-luna-')) {
return 'gpt-5.6-luna'
}
// Why: OpenAI routes the bare `gpt-5.6` alias to Sol. Match it exactly — a
// `gpt-5.6-` prefix match would swallow the tier IDs above and any future
// cheaper variant.
if (normalized === 'gpt-5.6') {
return 'gpt-5.6-sol'
}
return null
}