Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- [2026-09-23] price fable 5.1, opus 5.5, sonnet 5, gpt-6 and gpt-5.6 at current rates
- [2026-08-21] codex keeps its shell and apply_patch tools on the gpt-5.6 models
- [2026-08-18] meter clients that hang up early
- [2026-08-18] reclaim bare provider tables
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@
"post-commit": "bun x @rubriclab/package post-commit"
},
"type": "module",
"version": "0.0.66"
"version": "0.0.67"
}
4 changes: 0 additions & 4 deletions src/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,8 +533,6 @@ const UsageResponseSchema = z
limits: z.array(LimitSchema).nullish(),
seven_day: UsageWindowResponseSchema.nullish(),
seven_day_oauth_apps: UsageWindowResponseSchema.nullish(),
seven_day_opus: UsageWindowResponseSchema.nullish(),
seven_day_sonnet: UsageWindowResponseSchema.nullish(),
spend: SpendResponseSchema.nullish()
})
.passthrough()
Expand Down Expand Up @@ -661,8 +659,6 @@ async function fetchClaudeUsage(input: {
const definitions = [
['five_hour', '5 hour', 'session', body.five_hour],
['seven_day', '7 day', 'weekly_all', body.seven_day],
['seven_day_opus', '7 day · Opus', null, body.seven_day_opus],
['seven_day_sonnet', '7 day · Sonnet', null, body.seven_day_sonnet],
['seven_day_oauth_apps', '7 day · OAuth apps', null, body.seven_day_oauth_apps]
] as const
for (const [id, label, limitEquivalent, window] of definitions) {
Expand Down
7 changes: 3 additions & 4 deletions src/config-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,9 @@ function piModelsPath(): string {
const piProviderKeys = ['tokenmaxx-anthropic', 'tokenmaxx-openai']

// The anthropic ids pair with an API-key account (subscription auth is not for
// third-party harnesses); gpt-5.6-sol is the one id the ChatGPT codex backend
// accepts for subscription accounts.
const piAnthropicModelIds = ['claude-opus-4-8', 'claude-sonnet-4-6']
const piOpenaiModelIds = ['gpt-5.6-sol']
// third-party harnesses).
const piAnthropicModelIds = ['claude-fable-5-1', 'claude-opus-5-5', 'claude-sonnet-5']
const piOpenaiModelIds = ['gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna']

function piProviders(paths: ApplicationPaths): Record<string, unknown> {
const models = (ids: readonly string[]) => ids.map(id => ({ id, reasoning: true }))
Expand Down
34 changes: 28 additions & 6 deletions src/pricing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,45 @@ import { costUsd, priceFor } from './pricing.ts'

describe('priceFor', () => {
test('current model generations use current list prices', () => {
expect(priceFor('claude-fable-5')).toMatchObject({ inputPerMTok: 10, outputPerMTok: 50 })
expect(priceFor('claude-opus-4-8')).toMatchObject({ inputPerMTok: 5, outputPerMTok: 25 })
expect(priceFor('claude-sonnet-4-6')).toMatchObject({ inputPerMTok: 3, outputPerMTok: 15 })
expect(priceFor('claude-fable-5-1')).toMatchObject({ inputPerMTok: 10, outputPerMTok: 50 })
expect(priceFor('claude-opus-5-5')).toMatchObject({ inputPerMTok: 4, outputPerMTok: 20 })
expect(priceFor('claude-opus-5')).toMatchObject({ inputPerMTok: 5, outputPerMTok: 25 })
expect(priceFor('claude-sonnet-5')).toMatchObject({ inputPerMTok: 2, outputPerMTok: 10 })
expect(priceFor('claude-haiku-4-5-20251001')).toMatchObject({ inputPerMTok: 1, outputPerMTok: 5 })
expect(priceFor('gpt-5.6-sol')).toMatchObject({ inputPerMTok: 1.25, outputPerMTok: 10 })
expect(priceFor('gpt-6-astra')).toMatchObject({ inputPerMTok: 10, outputPerMTok: 50 })
expect(priceFor('gpt-6-sol')).toMatchObject({ inputPerMTok: 2, outputPerMTok: 10 })
expect(priceFor('gpt-6-luna')).toMatchObject({ inputPerMTok: 0.1, outputPerMTok: 0.5 })
expect(priceFor('gpt-5.6-sol')).toMatchObject({ inputPerMTok: 4, outputPerMTok: 20 })
expect(priceFor('gpt-5.6-terra')).toMatchObject({ inputPerMTok: 2, outputPerMTok: 12 })
expect(priceFor('gpt-5.6-luna')).toMatchObject({ inputPerMTok: 0.2, outputPerMTok: 1.2 })
})

test('legacy opus generations keep their historical 15/75 pricing', () => {
test('earlier generations keep their own prices', () => {
expect(priceFor('claude-fable-5')).toMatchObject({ cacheReadPerMTok: 1, inputPerMTok: 10 })
expect(priceFor('claude-opus-4-8')).toMatchObject({ inputPerMTok: 5, outputPerMTok: 25 })
expect(priceFor('claude-opus-4-1')).toMatchObject({ inputPerMTok: 15, outputPerMTok: 75 })
expect(priceFor('claude-opus-4-20250514')).toMatchObject({ inputPerMTok: 15, outputPerMTok: 75 })
expect(priceFor('claude-sonnet-4-6')).toMatchObject({ inputPerMTok: 3, outputPerMTok: 15 })
expect(priceFor('gpt-5.5')).toMatchObject({ inputPerMTok: 5, outputPerMTok: 30 })
expect(priceFor('gpt-5.3-codex')).toMatchObject({ inputPerMTok: 1.75, outputPerMTok: 14 })
expect(priceFor('gpt-5')).toMatchObject({ inputPerMTok: 1.25, outputPerMTok: 10 })
})

test('cache reads are a tenth of input, anthropic cache writes 1.25x', () => {
const opus = priceFor('claude-opus-4-8')
expect(opus.cacheReadPerMTok).toBeCloseTo(opus.inputPerMTok * 0.1)
expect(opus.cacheWritePerMTok).toBeCloseTo(opus.inputPerMTok * 1.25)
expect(priceFor('gpt-5.6-sol').cacheWritePerMTok).toBe(0)
})

test('fable 5.1 and opus 5.5 discount cache reads below a tenth of input', () => {
expect(priceFor('claude-fable-5-1').cacheReadPerMTok).toBe(0.25)
expect(priceFor('claude-opus-5-5').cacheReadPerMTok).toBe(0.2)
})

test('openai bills cache writes from gpt-5.6 on', () => {
expect(priceFor('gpt-6-sol').cacheWritePerMTok).toBe(2.5)
expect(priceFor('gpt-5.6-sol').cacheWritePerMTok).toBe(5)
expect(priceFor('gpt-5.5').cacheWritePerMTok).toBe(0)
})
})

Expand Down
44 changes: 44 additions & 0 deletions src/pricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ interface ModelPrice {
}

const PRICES: ReadonlyArray<{ match: string; price: ModelPrice }> = [
{
match: 'claude-fable-5-1',
price: { cacheReadPerMTok: 0.25, cacheWritePerMTok: 12.5, inputPerMTok: 10, outputPerMTok: 50 }
},
{
match: 'claude-fable',
price: { cacheReadPerMTok: 1, cacheWritePerMTok: 12.5, inputPerMTok: 10, outputPerMTok: 50 }
Expand All @@ -14,6 +18,10 @@ const PRICES: ReadonlyArray<{ match: string; price: ModelPrice }> = [
match: 'claude-mythos',
price: { cacheReadPerMTok: 1, cacheWritePerMTok: 12.5, inputPerMTok: 10, outputPerMTok: 50 }
},
{
match: 'claude-opus-5-5',
price: { cacheReadPerMTok: 0.2, cacheWritePerMTok: 5, inputPerMTok: 4, outputPerMTok: 20 }
},
{
match: 'claude-opus-4-1',
price: { cacheReadPerMTok: 1.5, cacheWritePerMTok: 18.75, inputPerMTok: 15, outputPerMTok: 75 }
Expand All @@ -26,6 +34,10 @@ const PRICES: ReadonlyArray<{ match: string; price: ModelPrice }> = [
match: 'claude-opus',
price: { cacheReadPerMTok: 0.5, cacheWritePerMTok: 6.25, inputPerMTok: 5, outputPerMTok: 25 }
},
{
match: 'claude-sonnet-5',
price: { cacheReadPerMTok: 0.2, cacheWritePerMTok: 2.5, inputPerMTok: 2, outputPerMTok: 10 }
},
{
match: 'claude-sonnet',
price: { cacheReadPerMTok: 0.3, cacheWritePerMTok: 3.75, inputPerMTok: 3, outputPerMTok: 15 }
Expand All @@ -34,6 +46,38 @@ const PRICES: ReadonlyArray<{ match: string; price: ModelPrice }> = [
match: 'claude-haiku',
price: { cacheReadPerMTok: 0.1, cacheWritePerMTok: 1.25, inputPerMTok: 1, outputPerMTok: 5 }
},
{
match: 'gpt-6-astra',
price: { cacheReadPerMTok: 1, cacheWritePerMTok: 12.5, inputPerMTok: 10, outputPerMTok: 50 }
},
{
match: 'gpt-6-sol',
price: { cacheReadPerMTok: 0.2, cacheWritePerMTok: 2.5, inputPerMTok: 2, outputPerMTok: 10 }
},
{
match: 'gpt-6-luna',
price: { cacheReadPerMTok: 0.01, cacheWritePerMTok: 0.125, inputPerMTok: 0.1, outputPerMTok: 0.5 }
},
{
match: 'gpt-5.6-sol',
price: { cacheReadPerMTok: 0.4, cacheWritePerMTok: 5, inputPerMTok: 4, outputPerMTok: 20 }
},
{
match: 'gpt-5.6-terra',
price: { cacheReadPerMTok: 0.2, cacheWritePerMTok: 2.5, inputPerMTok: 2, outputPerMTok: 12 }
},
{
match: 'gpt-5.6-luna',
price: { cacheReadPerMTok: 0.02, cacheWritePerMTok: 0.25, inputPerMTok: 0.2, outputPerMTok: 1.2 }
},
{
match: 'gpt-5.5',
price: { cacheReadPerMTok: 0.5, cacheWritePerMTok: 0, inputPerMTok: 5, outputPerMTok: 30 }
},
{
match: 'gpt-5.3-codex',
price: { cacheReadPerMTok: 0.175, cacheWritePerMTok: 0, inputPerMTok: 1.75, outputPerMTok: 14 }
},
{
match: 'gpt-5',
price: { cacheReadPerMTok: 0.125, cacheWritePerMTok: 0, inputPerMTok: 1.25, outputPerMTok: 10 }
Expand Down
8 changes: 4 additions & 4 deletions src/tui/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ function buildTokens(scale: number): TokenAnalytics {
const buckets = raw.map(value => Math.round((value / rawSum) * target))
const totalTokens = buckets.reduce((sum, value) => sum + value, 0)
const modelMix: { model: string; provider: ProviderId; share: number }[] = [
{ model: 'gpt-5.6-sol', provider: 'openai', share: 0.42 },
{ model: 'gpt-5.6-codex', provider: 'openai', share: 0.13 },
{ model: 'claude-opus-4-8', provider: 'anthropic', share: 0.3 },
{ model: 'claude-sonnet-4-6', provider: 'anthropic', share: 0.11 },
{ model: 'gpt-6-sol', provider: 'openai', share: 0.42 },
{ model: 'gpt-6-astra', provider: 'openai', share: 0.13 },
{ model: 'claude-opus-5-5', provider: 'anthropic', share: 0.3 },
{ model: 'claude-sonnet-5', provider: 'anthropic', share: 0.11 },
{ model: 'claude-haiku-4-5', provider: 'anthropic', share: 0.04 }
]
const models = modelMix
Expand Down
3 changes: 1 addition & 2 deletions src/tui/usage-windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ export function isFiveHourUsageWindow(window: UsageWindow): boolean {
}

function usageWindowPriority(window: UsageWindow): number {
const identity = `${window.id} ${window.label}`
switch (true) {
case isFiveHourUsageWindow(window):
return 0
case /^(7 day(?: · all models)?)$/i.test(window.label):
case /^(weekly_all|seven_day|weekly|codex:primary)$/.test(window.id):
return 1
case /scoped|fable|opus|sonnet|spark/i.test(identity):
case /^(weekly_scoped|codex_)/.test(window.id):
return 2
default:
return 3
Expand Down
Loading