Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Add the day's estimated API value to `hourly-summary.json`, so a bar widget can show it without running a report. `value` prices the same events `tokens` counts, the way the dashboard's report prices them, and `unpricedTokens` is the part no rate covers. The file keeps schema version 1, and readers that ignore the new fields see no change.

## 1.11.2 - 2026-10-08

- Colors work again on Qt 6.12. QtQuick now ships its own `Color` type, which hid the
Expand Down
16 changes: 14 additions & 2 deletions collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2461,6 +2461,11 @@ def hourly_snapshot(ledger, now=None, cfg=None):
Totals are kept per provider and per agent record. A labelled second login
is its own record in the panel but shares its provider's table with the
main login, so the record totals split each provider by account.

`value` is the estimated API value of the same events `tokens` counts,
priced as `report` prices them, and `unpricedTokens` the part of `tokens`
no rate covers. Bar widgets can show the day's value from this file
instead of running a report.
"""
cfg = cfg or DEFAULTS
now = now or dt.datetime.now().astimezone()
Expand All @@ -2481,9 +2486,12 @@ def hourly_snapshot(ledger, now=None, cfg=None):
for key, (provs, account, _) in targets.items():
for provider in provs: records.setdefault((provider, account), []).append(key)
resolve, accounts = source_account(cfg), {}
total = timed = unplaced = 0
total = timed = unplaced = unpriced = 0
value_total = 0.0
rates = load_rates()
ledger.db.row_factory = sqlite3.Row
for row in ledger.db.execute('SELECT e.provider,e.client,e.timePrecision,e.ts,e.input,e.output,e.cacheRead,e.cacheWrite,'
for row in ledger.db.execute('SELECT e.provider,e.client,e.timePrecision,e.ts,e.model,e.apiProvider,e.reportedValue,e.reportedCostTicks,'
'e.serviceTier,e.requestedServiceTier,e.input,e.output,e.cacheRead,e.cacheWrite,e.cacheWrite1h,'
'group_concat(s.path,char(31)) AS paths '
'FROM events e INDEXED BY events_time LEFT JOIN event_sources s ON s.event_id=e.id '
'WHERE e.ts>=? AND e.ts<=? GROUP BY e.id', (start, end)):
Expand All @@ -2501,6 +2509,9 @@ def hourly_snapshot(ledger, now=None, cfg=None):
total += tokens
p['tokens'] += tokens
for m in mine: m['tokens'] += tokens
value = price(r, rates['document'])[0]
if value is None: unpriced += tokens
else: value_total += value
if not timed_event(r):
unplaced += tokens
p['unplacedTokens'] += tokens
Expand All @@ -2519,6 +2530,7 @@ def hourly_snapshot(ledger, now=None, cfg=None):
return {'schemaVersion': 1, 'date': str(day), 'generatedAt': now.timestamp(),
'timeZone': now.tzname() or '', 'utcOffsetMinutes': int(now.utcoffset().total_seconds() // 60),
'tokens': total, 'timedTokens': timed, 'unplacedTokens': unplaced,
'value': value_total, 'unpricedTokens': unpriced,
'availableProviders': available_providers, 'providers': providers, 'hours': hours,
# The same totals for each agent record, so a second login shows
# its own usage instead of its provider's, and the records with
Expand Down
21 changes: 21 additions & 0 deletions tests/test_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -1794,6 +1794,27 @@ def test_drilldown_reconciles_sessions_and_partial_pricing(self):
self.assertIsNone(empty['pricing']['coveragePercent'])
ledger.db.close()

def test_hourly_snapshot_carries_the_days_value_as_report_prices_it(self):
ledger = c.Ledger(self.root / 'value.sqlite')
now = dt.datetime(2026, 9, 5, 12).astimezone()
for key, model, when, inp in [('a', 'known', '2026-09-05T10:00:00', 100), ('b', 'unknown', '2026-09-05T10:30:00', 200),
('c', 'known', '2026-09-05T11:00:00', 300), ('prior', 'known', '2026-09-04T10:00:00', 700)]:
ledger.put(c.record(key, 'codex', key, when, model, '', 'CLI', input=inp))
# A Fast turn is priced at its tier's rate, in the snapshot as in the report.
ledger.put({**c.record('fast', 'codex', 'fast', '2026-09-05T11:30:00', 'known', '', 'CLI', input=50),
'requestedServiceTier': 'priority'})
rates = {'source': 'test', 'document': {'known': {'input_cost_per_token': 0.01, 'input_cost_per_token_priority': 0.02}}}
with patch.object(c, 'load_rates', return_value=rates):
snapshot = c.hourly_snapshot(ledger, now)
day = c.report(ledger, c.DEFAULTS, 1, now=now)
self.assertEqual((snapshot['tokens'], snapshot['value'], snapshot['unpricedTokens']), (650, 5, 200))
self.assertEqual((snapshot['tokens'], snapshot['value'], snapshot['unpricedTokens']),
(day['summary']['tokens'], day['summary']['value'], day['summary']['unpricedTokens']))
with patch.object(c, 'load_rates', return_value={'source': 'test', 'document': {}}):
unpriced = c.hourly_snapshot(ledger, now)
self.assertEqual((unpriced['value'], unpriced['unpricedTokens']), (0, 650))
ledger.db.close()

def test_missing_source_does_not_claim_fresh_history(self):
ledger = c.Ledger(self.root / 'coverage.sqlite')
with patch.object(c, 'HOME', self.root), patch.dict('os.environ', {
Expand Down
Loading