diff --git a/hub/src/store/usage.ts b/hub/src/store/usage.ts index 8c49ca33..3c3644fe 100644 --- a/hub/src/store/usage.ts +++ b/hub/src/store/usage.ts @@ -128,6 +128,13 @@ export function recordUsageScan( last_cache_creation_tokens = excluded.last_cache_creation_tokens WHERE usage_events.kind = 'delta' `) + const updateCumulativeModel = db.prepare(` + UPDATE usage_events + SET model = ? + WHERE session_id = ? + AND source_key = ? + AND kind = 'cumulative' + `) for (const event of events) { statement.run({ @@ -147,6 +154,9 @@ export function recordUsageScan( last_cache_read_tokens: event.lastCacheReadTokens, last_cache_creation_tokens: event.lastCacheCreationTokens }) + if (event.kind === 'cumulative' && event.model !== null) { + updateCumulativeModel.run(event.model, event.sessionId, event.sourceKey) + } } } diff --git a/hub/src/sync/usageService.test.ts b/hub/src/sync/usageService.test.ts index 23cf31d6..c41dd1c6 100644 --- a/hub/src/sync/usageService.test.ts +++ b/hub/src/sync/usageService.test.ts @@ -256,6 +256,43 @@ describe('usage service', () => { store.close() }) + it('replaces a cumulative fallback model when a replay adds an explicit model', () => { + const store = new Store(':memory:') + const session = store.sessions.getOrCreateSession( + 'codex-explicit-model-replay-test', + { path: '/tmp', host: 'test', flavor: 'codex' }, + null, + 'default', + 'deepseek-v4-flash' + ) + const tokenCount = (model?: string) => ({ + type: 'codex', + data: { + type: 'token_count', + model, + thread_id: 'thread-1', + turn_id: 'turn-1', + info: { + total_token_usage: { input_tokens: 100, output_tokens: 10 }, + last_token_usage: { input_tokens: 100, output_tokens: 10 } + } + } + }) + + addAgentMessage(store, session.id, tokenCount()) + expect(getUsageSummary(store, 'default', 'all').byModel).toEqual([ + expect.objectContaining({ key: 'deepseek-v4-flash', totalTokens: 110, requests: 1 }) + ]) + + addAgentMessage(store, session.id, tokenCount('gpt-5.6-sol')) + const result = getUsageSummary(store, 'default', 'all') + expect(result.totals).toEqual(expect.objectContaining({ totalTokens: 110, requests: 1 })) + expect(result.byModel).toEqual([ + expect.objectContaining({ key: 'gpt-5.6-sol', totalTokens: 110, requests: 1 }) + ]) + store.close() + }) + it('preserves event-level models across model switches and epoch rebuilds', () => { const store = new Store(':memory:') const session = store.sessions.getOrCreateSession(