diff --git a/src/main/claude-usage/store.test.ts b/src/main/claude-usage/store.test.ts index 96e21692d0d..85d85d8bac9 100644 --- a/src/main/claude-usage/store.test.ts +++ b/src/main/claude-usage/store.test.ts @@ -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 () => { diff --git a/src/main/codex-usage/store.test.ts b/src/main/codex-usage/store.test.ts index 58bc58cf056..a20472abd25 100644 --- a/src/main/codex-usage/store.test.ts +++ b/src/main/codex-usage/store.test.ts @@ -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: [ diff --git a/src/main/codex-usage/store.ts b/src/main/codex-usage/store.ts index 630acd6c468..f3c62fb8973 100644 --- a/src/main/codex-usage/store.ts +++ b/src/main/codex-usage/store.ts @@ -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 }