feat(usage): price Claude 5 family and GPT-5.6 token usage

Claude Opus 5, Sonnet 5, Fable 5 and Codex gpt-5.6 sol/terra/luna were
absent from the usage pricing tables, so their turns aggregated tokens
but reported no estimated cost.

Rates from Anthropic and OpenAI published pricing. Sonnet 5 gets no
long-context tier: Claude 4.6 and later bill the full 1M window flat.

Sonnet 5 uses the standard $3/$15 rate, not the $2/$10 introductory rate
that runs through 2026-08-31 — the table has no date dimension.
This commit is contained in:
Brennan Benson
2026-07-26 18:36:05 -07:00
parent 4ada3f8b2c
commit b4f0cdeba1
4 changed files with 219 additions and 0 deletions
+112
View File
@@ -254,6 +254,118 @@ describe('ClaudeUsageStore', () => {
).toBeCloseTo(36.75)
})
it('prices Claude 5 family models with current Anthropic rates', async () => {
const store = createStoreWithState({
dailyAggregates: [
{
day: '2026-04-09',
model: 'claude-opus-5',
projectKey: 'worktree:repo-1::/workspace/repo-a',
projectLabel: 'Repo A',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo-a',
turnCount: 1,
zeroCacheReadTurnCount: 0,
inputTokens: 1_000_000,
outputTokens: 1_000_000,
cacheReadTokens: 1_000_000,
cacheWriteTokens: 1_000_000
},
{
day: '2026-04-09',
model: 'anthropic/claude-fable-5',
projectKey: 'worktree:repo-1::/workspace/repo-a',
projectLabel: 'Repo A',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo-a',
turnCount: 1,
zeroCacheReadTurnCount: 0,
inputTokens: 1_000_000,
outputTokens: 1_000_000,
cacheReadTokens: 1_000_000,
cacheWriteTokens: 1_000_000
},
{
day: '2026-04-09',
model: 'claude-sonnet-5-thinking',
projectKey: 'worktree:repo-1::/workspace/repo-a',
projectLabel: 'Repo A',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo-a',
turnCount: 1,
zeroCacheReadTurnCount: 0,
inputTokens: 1_000_000,
outputTokens: 1_000_000,
cacheReadTokens: 1_000_000,
cacheWriteTokens: 1_000_000
}
]
})
const breakdown = await store.getBreakdown('orca', '30d', 'model')
expect(breakdown.find((row) => row.key === 'claude-opus-5')?.estimatedCostUsd).toBeCloseTo(
36.75
)
expect(
breakdown.find((row) => row.key === 'anthropic/claude-fable-5')?.estimatedCostUsd
).toBeCloseTo(73.5)
expect(
breakdown.find((row) => row.key === 'claude-sonnet-5-thinking')?.estimatedCostUsd
).toBeCloseTo(22.05)
})
it('prices Sonnet 5 long-context usage at flat rates', async () => {
const store = createStoreWithState({
dailyAggregates: [
{
day: '2026-04-09',
model: 'claude-sonnet-5',
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')
// Why: Sonnet 4.6 and earlier bill above 200k at a premium; Sonnet 5 does not.
expect(summary.estimatedCostUsd).toBeCloseTo(6.615)
})
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
}
]
})
const summary = await store.getSummary('orca', '30d')
expect(summary.estimatedCostUsd).toBeCloseTo(8.07)
})
it('prices unknown newer Opus 4 point releases with current Opus rates', async () => {
const store = createStoreWithState({
dailyAggregates: [
+13
View File
@@ -59,6 +59,10 @@ const SONNET_LONG_CONTEXT_PRICING = {
} satisfies Partial<ClaudeModelPricing>
const MODEL_PRICING: Record<string, ClaudeModelPricing> = {
'claude-fable-5': { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
'claude-opus-5': { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
// Why: Sonnet 5 bills its full 1M window at flat rates, so no long-context tier here.
'claude-sonnet-5': { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
'claude-opus-4-8': { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
'claude-opus-4-7': { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
'claude-opus-4-6': { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
@@ -156,6 +160,12 @@ function normalizeModelForPricing(model: string | null): string | null {
if (alias) {
return alias
}
if (hasClaudeModelVersion(lower, 'fable', '5')) {
return 'claude-fable-5'
}
if (hasClaudeModelVersion(lower, 'opus', '5')) {
return 'claude-opus-5'
}
if (hasClaudeModelVersion(lower, 'opus', '4-8')) {
return 'claude-opus-4-8'
}
@@ -179,6 +189,9 @@ function normalizeModelForPricing(model: string | null): string | null {
// avoid overbilling unknown future Claude Code model IDs as legacy Opus 4.
return 'claude-opus-4-8'
}
if (hasClaudeModelVersion(lower, 'sonnet', '5')) {
return 'claude-sonnet-5'
}
if (hasClaudeModelVersion(lower, 'sonnet', '4-6')) {
return 'claude-sonnet-4-6'
}
+61
View File
@@ -364,6 +364,67 @@ describe('CodexUsageStore', () => {
expect(breakdown.find((row) => row.key === 'gpt-5.5')?.estimatedCostUsd).toBeCloseTo(50.424)
})
it('prices GPT-5.6 sol, terra, and luna with current OpenAI rates', async () => {
const store = createStoreWithState({
dailyAggregates: ['gpt-5.6-sol', 'gpt-5.6-terra', '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: 2_000_000,
cachedInputTokens: 1_000_000,
outputTokens: 1_000_000,
reasoningOutputTokens: 100_000,
totalTokens: 3_000_000,
hasInferredPricing: false
}))
})
const summary = await store.getSummary('orca', '30d')
const breakdown = await store.getBreakdown('orca', '30d', 'model')
expect(summary.estimatedCostUsd).toBeCloseTo(85.7208)
expect(breakdown.find((row) => row.key === 'gpt-5.6-sol')?.estimatedCostUsd).toBeCloseTo(50.424)
expect(breakdown.find((row) => row.key === 'gpt-5.6-terra')?.estimatedCostUsd).toBeCloseTo(
25.212
)
expect(breakdown.find((row) => row.key === 'gpt-5.6-luna')?.estimatedCostUsd).toBeCloseTo(
10.0848
)
})
it('normalizes GPT-5.6 reasoning suffixes before pricing', async () => {
const store = createStoreWithState({
dailyAggregates: ['gpt-5.6-terra-high', 'gpt-5.6-luna(medium)'].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-terra-high')?.estimatedCostUsd).toBeCloseTo(
0.5125
)
expect(
breakdown.find((row) => row.key === 'gpt-5.6-luna(medium)')?.estimatedCostUsd
).toBeCloseTo(0.205)
})
it('normalizes Codex model variants and reasoning suffixes before pricing', async () => {
const store = createStoreWithState({
dailyAggregates: [
+33
View File
@@ -90,6 +90,30 @@ const MODEL_PRICING: Record<string, CodexModelPricing> = {
inputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 10 }],
cachedInputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 1 }],
outputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 45 }]
},
'gpt-5.6-sol': {
input: 5,
cachedInput: 0.5,
output: 30,
inputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 10 }],
cachedInputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 1 }],
outputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 45 }]
},
'gpt-5.6-terra': {
input: 2.5,
cachedInput: 0.25,
output: 15,
inputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 5 }],
cachedInputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 0.5 }],
outputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 22.5 }]
},
'gpt-5.6-luna': {
input: 1,
cachedInput: 0.1,
output: 6,
inputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 2 }],
cachedInputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 0.2 }],
outputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 9 }]
}
}
@@ -228,6 +252,15 @@ function normalizeModelForPricing(model: string | null): string | null {
if (normalized === 'gpt-5.5' || normalized.startsWith('gpt-5.5-')) {
return 'gpt-5.5'
}
if (normalized === 'gpt-5.6-sol' || normalized.startsWith('gpt-5.6-sol-')) {
return 'gpt-5.6-sol'
}
if (normalized === 'gpt-5.6-terra' || normalized.startsWith('gpt-5.6-terra-')) {
return 'gpt-5.6-terra'
}
if (normalized === 'gpt-5.6-luna' || normalized.startsWith('gpt-5.6-luna-')) {
return 'gpt-5.6-luna'
}
return null
}