Skip to content

Commit 6b65429

Browse files
committed
merge main
2 parents 59938b6 + 4c11d3e commit 6b65429

10 files changed

Lines changed: 149 additions & 26 deletions

‎CHANGELOG.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
- [2026-09-25] pick which accounts auto-rotation uses first
1+
- [2026-09-30] toml fallback test survives newer bun
2+
- [2026-09-30] [settings shows live routing state](https://github.com/RubricLab/tokenmaxx/commit/df576cf609f72bf274e932fe1bd8d74ed191cc7d)
3+
- [2026-09-30] [pick which accounts auto-rotation uses first](https://github.com/RubricLab/tokenmaxx/commit/8999da2b9c11f204d5b2d932b64fb221e52646dc)
4+
- [2026-09-23] price fable 5.1, opus 5.5, sonnet 5, gpt-6 and gpt-5.6 at current rates
25
- [2026-08-21] codex keeps its shell and apply_patch tools on the gpt-5.6 models
36
- [2026-08-18] meter clients that hang up early
47
- [2026-08-18] reclaim bare provider tables

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,5 @@
5959
"post-commit": "bun x @rubriclab/package post-commit"
6060
},
6161
"type": "module",
62-
"version": "0.0.67"
62+
"version": "0.0.71"
6363
}

‎src/claude.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,6 @@ const UsageResponseSchema = z
533533
limits: z.array(LimitSchema).nullish(),
534534
seven_day: UsageWindowResponseSchema.nullish(),
535535
seven_day_oauth_apps: UsageWindowResponseSchema.nullish(),
536-
seven_day_opus: UsageWindowResponseSchema.nullish(),
537-
seven_day_sonnet: UsageWindowResponseSchema.nullish(),
538536
spend: SpendResponseSchema.nullish()
539537
})
540538
.passthrough()
@@ -661,8 +659,6 @@ async function fetchClaudeUsage(input: {
661659
const definitions = [
662660
['five_hour', '5 hour', 'session', body.five_hour],
663661
['seven_day', '7 day', 'weekly_all', body.seven_day],
664-
['seven_day_opus', '7 day · Opus', null, body.seven_day_opus],
665-
['seven_day_sonnet', '7 day · Sonnet', null, body.seven_day_sonnet],
666662
['seven_day_oauth_apps', '7 day · OAuth apps', null, body.seven_day_oauth_apps]
667663
] as const
668664
for (const [id, label, limitEquivalent, window] of definitions) {

‎src/config-install.test.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,45 @@ describe('installCodexConfig', () => {
8989
expect(status.codexStale).toBe(false)
9090
})
9191

92+
test('installStatus stays truthful when Bun.TOML cannot parse the config', async () => {
93+
// Bun.TOML rejects bare table keys that start with a digit, like
94+
// [mcp_servers.1password]; codex accepts them. A parse failure must not
95+
// read as "not routed" while our selection line is active.
96+
await writeCodexConfig(
97+
`${legacyBrokenConfig}\n[mcp_servers.1password]\ncommand = "1password-mcp"\nenabled = false\n`
98+
)
99+
await installCodexConfig(paths())
100+
const status = await installStatus()
101+
expect(status.codexRouted).toBe(true)
102+
})
103+
104+
test('an unparseable config without our selection still reads as not routed', async () => {
105+
await writeCodexConfig(
106+
'model = "gpt-5.6-sol"\n\n[mcp_servers.1password]\ncommand = "1password-mcp"\n'
107+
)
108+
const status = await installStatus()
109+
expect(status.codexRouted).toBe(false)
110+
})
111+
112+
test('the fallback ignores a swallowed legacy selection under a table', async () => {
113+
// legacyBrokenConfig's model_provider = "tokmax" sits under [notice], so
114+
// codex never routes through it; the digit table only breaks parsing.
115+
await writeCodexConfig(
116+
`${legacyBrokenConfig}\n[mcp_servers.1password]\ncommand = "1password-mcp"\n`
117+
)
118+
const status = await installStatus()
119+
expect(status.codexRouted).toBe(false)
120+
expect(status.codexStale).toBe(true)
121+
})
122+
123+
test('the fallback ignores our provider named inside a codex profile', async () => {
124+
await writeCodexConfig(
125+
'model_provider = "ollama"\n\n[profiles.work]\nmodel_provider = "tokenmaxx"\n\n[mcp_servers.1password]\ncommand = "1password-mcp"\n'
126+
)
127+
const status = await installStatus()
128+
expect(status.codexRouted).toBe(false)
129+
})
130+
92131
test('reinstall is idempotent', async () => {
93132
await writeCodexConfig(legacyBrokenConfig)
94133
await installCodexConfig(paths())

‎src/config-install.ts‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,15 @@ export async function installStatus(): Promise<InstallStatus> {
196196
const baseUrl = selected === null ? undefined : parsed.model_providers?.[selected]?.base_url
197197
codexRouted = typeof baseUrl === 'string' && baseUrl.includes('127.0.0.1')
198198
} catch {
199-
codexRouted = false
199+
// Bun.TOML rejects configs codex accepts — bare table keys starting with a
200+
// digit, like [mcp_servers.1password]. Reading that as "not routed" makes
201+
// the dashboard show routing off while traffic flows through the proxy,
202+
// and turns the routing toggle into a re-install. Fall back to our own
203+
// active selection line, scanning only the top-level region: a
204+
// model_provider line under a table belongs to that table, not to codex.
205+
const firstTable = codexRaw.search(/^\[/m)
206+
const topLevel = firstTable === -1 ? codexRaw : codexRaw.slice(0, firstTable)
207+
codexRouted = topLevel.split('\n').some(line => ownProviderSelection.test(line))
200208
}
201209
const codexStale =
202210
!codexRouted &&
@@ -249,10 +257,9 @@ function piModelsPath(): string {
249257
const piProviderKeys = ['tokenmaxx-anthropic', 'tokenmaxx-openai']
250258

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

257264
function piProviders(paths: ApplicationPaths): Record<string, unknown> {
258265
const models = (ids: readonly string[]) => ids.map(id => ({ id, reasoning: true }))

‎src/pricing.test.ts‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,45 @@ import { costUsd, priceFor } from './pricing.ts'
33

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

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

1830
test('cache reads are a tenth of input, anthropic cache writes 1.25x', () => {
1931
const opus = priceFor('claude-opus-4-8')
2032
expect(opus.cacheReadPerMTok).toBeCloseTo(opus.inputPerMTok * 0.1)
2133
expect(opus.cacheWritePerMTok).toBeCloseTo(opus.inputPerMTok * 1.25)
22-
expect(priceFor('gpt-5.6-sol').cacheWritePerMTok).toBe(0)
34+
})
35+
36+
test('fable 5.1 and opus 5.5 discount cache reads below a tenth of input', () => {
37+
expect(priceFor('claude-fable-5-1').cacheReadPerMTok).toBe(0.25)
38+
expect(priceFor('claude-opus-5-5').cacheReadPerMTok).toBe(0.2)
39+
})
40+
41+
test('openai bills cache writes from gpt-5.6 on', () => {
42+
expect(priceFor('gpt-6-sol').cacheWritePerMTok).toBe(2.5)
43+
expect(priceFor('gpt-5.6-sol').cacheWritePerMTok).toBe(5)
44+
expect(priceFor('gpt-5.5').cacheWritePerMTok).toBe(0)
2345
})
2446
})
2547

‎src/pricing.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ interface ModelPrice {
66
}
77

88
const PRICES: ReadonlyArray<{ match: string; price: ModelPrice }> = [
9+
{
10+
match: 'claude-fable-5-1',
11+
price: { cacheReadPerMTok: 0.25, cacheWritePerMTok: 12.5, inputPerMTok: 10, outputPerMTok: 50 }
12+
},
913
{
1014
match: 'claude-fable',
1115
price: { cacheReadPerMTok: 1, cacheWritePerMTok: 12.5, inputPerMTok: 10, outputPerMTok: 50 }
@@ -14,6 +18,10 @@ const PRICES: ReadonlyArray<{ match: string; price: ModelPrice }> = [
1418
match: 'claude-mythos',
1519
price: { cacheReadPerMTok: 1, cacheWritePerMTok: 12.5, inputPerMTok: 10, outputPerMTok: 50 }
1620
},
21+
{
22+
match: 'claude-opus-5-5',
23+
price: { cacheReadPerMTok: 0.2, cacheWritePerMTok: 5, inputPerMTok: 4, outputPerMTok: 20 }
24+
},
1725
{
1826
match: 'claude-opus-4-1',
1927
price: { cacheReadPerMTok: 1.5, cacheWritePerMTok: 18.75, inputPerMTok: 15, outputPerMTok: 75 }
@@ -26,6 +34,10 @@ const PRICES: ReadonlyArray<{ match: string; price: ModelPrice }> = [
2634
match: 'claude-opus',
2735
price: { cacheReadPerMTok: 0.5, cacheWritePerMTok: 6.25, inputPerMTok: 5, outputPerMTok: 25 }
2836
},
37+
{
38+
match: 'claude-sonnet-5',
39+
price: { cacheReadPerMTok: 0.2, cacheWritePerMTok: 2.5, inputPerMTok: 2, outputPerMTok: 10 }
40+
},
2941
{
3042
match: 'claude-sonnet',
3143
price: { cacheReadPerMTok: 0.3, cacheWritePerMTok: 3.75, inputPerMTok: 3, outputPerMTok: 15 }
@@ -34,6 +46,38 @@ const PRICES: ReadonlyArray<{ match: string; price: ModelPrice }> = [
3446
match: 'claude-haiku',
3547
price: { cacheReadPerMTok: 0.1, cacheWritePerMTok: 1.25, inputPerMTok: 1, outputPerMTok: 5 }
3648
},
49+
{
50+
match: 'gpt-6-astra',
51+
price: { cacheReadPerMTok: 1, cacheWritePerMTok: 12.5, inputPerMTok: 10, outputPerMTok: 50 }
52+
},
53+
{
54+
match: 'gpt-6-sol',
55+
price: { cacheReadPerMTok: 0.2, cacheWritePerMTok: 2.5, inputPerMTok: 2, outputPerMTok: 10 }
56+
},
57+
{
58+
match: 'gpt-6-luna',
59+
price: { cacheReadPerMTok: 0.01, cacheWritePerMTok: 0.125, inputPerMTok: 0.1, outputPerMTok: 0.5 }
60+
},
61+
{
62+
match: 'gpt-5.6-sol',
63+
price: { cacheReadPerMTok: 0.4, cacheWritePerMTok: 5, inputPerMTok: 4, outputPerMTok: 20 }
64+
},
65+
{
66+
match: 'gpt-5.6-terra',
67+
price: { cacheReadPerMTok: 0.2, cacheWritePerMTok: 2.5, inputPerMTok: 2, outputPerMTok: 12 }
68+
},
69+
{
70+
match: 'gpt-5.6-luna',
71+
price: { cacheReadPerMTok: 0.02, cacheWritePerMTok: 0.25, inputPerMTok: 0.2, outputPerMTok: 1.2 }
72+
},
73+
{
74+
match: 'gpt-5.5',
75+
price: { cacheReadPerMTok: 0.5, cacheWritePerMTok: 0, inputPerMTok: 5, outputPerMTok: 30 }
76+
},
77+
{
78+
match: 'gpt-5.3-codex',
79+
price: { cacheReadPerMTok: 0.175, cacheWritePerMTok: 0, inputPerMTok: 1.75, outputPerMTok: 14 }
80+
},
3781
{
3882
match: 'gpt-5',
3983
price: { cacheReadPerMTok: 0.125, cacheWritePerMTok: 0, inputPerMTok: 1.25, outputPerMTok: 10 }

‎src/tui/dashboard.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { Box, createCliRenderer, parseColor, type RGBA, Text } from '@opentui/core'
2-
import { installPiConfig, type PiStatus, piStatus, uninstallPiConfig } from '../config-install.ts'
2+
import {
3+
installPiConfig,
4+
installStatus,
5+
type PiStatus,
6+
piStatus,
7+
uninstallPiConfig
8+
} from '../config-install.ts'
39
import type {
410
Account,
511
AnalyticsSnapshot,
@@ -1358,6 +1364,11 @@ export async function runTuiDashboard(
13581364
: buildScenario(fixture.name, simulatedNow)
13591365
let rows = orderedRows(analytics.snapshot)
13601366
let pi: PiStatus = live ? await piStatus() : { present: true, routed: true }
1367+
// Routing is derived from the harness config files, which can change while
1368+
// this dashboard is open (tokenmaxx install/uninstall from another shell,
1369+
// first-login auto-enable, daemon heal after an update). options.routing is
1370+
// only the launch-time snapshot; reload() keeps this current.
1371+
let routing = options.routing
13611372
const state: ViewState = {
13621373
addConfirm: null,
13631374
alert: options.alert ?? '',
@@ -1398,7 +1409,7 @@ export async function runTuiDashboard(
13981409
columns,
13991410
now: live ? Date.now() : simulatedNow,
14001411
pi,
1401-
routing: options.routing,
1412+
routing,
14021413
rows: process.stdout.rows ?? 24,
14031414
switchFlagMs: fixture !== undefined && fixture.timewarp > 0 ? 24 * 60_000 : 120_000,
14041415
theme: currentTheme(),
@@ -1447,6 +1458,8 @@ export async function runTuiDashboard(
14471458
analytics = await readAnalytics(socketPath)
14481459
rows = orderedRows(analytics.snapshot)
14491460
pi = await piStatus()
1461+
const status = await installStatus()
1462+
routing = { anthropic: status.claudeRouted, openai: status.codexRouted }
14501463
clampSelection()
14511464
})
14521465

@@ -1608,7 +1621,7 @@ export async function runTuiDashboard(
16081621
}
16091622

16101623
const toggleRouting = (provider: ProviderId) => {
1611-
finish({ enable: !options.routing[provider], kind: 'routing', provider })
1624+
finish({ enable: !routing[provider], kind: 'routing', provider })
16121625
}
16131626

16141627
const adjustSetting = (delta: number) => {

‎src/tui/fixtures.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ function buildTokens(scale: number): TokenAnalytics {
3131
const buckets = raw.map(value => Math.round((value / rawSum) * target))
3232
const totalTokens = buckets.reduce((sum, value) => sum + value, 0)
3333
const modelMix: { model: string; provider: ProviderId; share: number }[] = [
34-
{ model: 'gpt-5.6-sol', provider: 'openai', share: 0.42 },
35-
{ model: 'gpt-5.6-codex', provider: 'openai', share: 0.13 },
36-
{ model: 'claude-opus-4-8', provider: 'anthropic', share: 0.3 },
37-
{ model: 'claude-sonnet-4-6', provider: 'anthropic', share: 0.11 },
34+
{ model: 'gpt-6-sol', provider: 'openai', share: 0.42 },
35+
{ model: 'gpt-6-astra', provider: 'openai', share: 0.13 },
36+
{ model: 'claude-opus-5-5', provider: 'anthropic', share: 0.3 },
37+
{ model: 'claude-sonnet-5', provider: 'anthropic', share: 0.11 },
3838
{ model: 'claude-haiku-4-5', provider: 'anthropic', share: 0.04 }
3939
]
4040
const models = modelMix

‎src/tui/usage-windows.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ export function isFiveHourUsageWindow(window: UsageWindow): boolean {
55
}
66

77
function usageWindowPriority(window: UsageWindow): number {
8-
const identity = `${window.id} ${window.label}`
98
switch (true) {
109
case isFiveHourUsageWindow(window):
1110
return 0
1211
case /^(7 day(?: · all models)?)$/i.test(window.label):
1312
case /^(weekly_all|seven_day|weekly|codex:primary)$/.test(window.id):
1413
return 1
15-
case /scoped|fable|opus|sonnet|spark/i.test(identity):
14+
case /^(weekly_scoped|codex_)/.test(window.id):
1615
return 2
1716
default:
1817
return 3

0 commit comments

Comments
 (0)