fix(pricing): book daily cost to the day tokens were spent

token_usage holds a session's cumulative totals with no time, so
/api/pricing/cost booked a multi-day session's whole cost to its start
date and the status bar's Today read ~$0. TranscriptCache now also splits
usage by each message's UTC hour (tokensByHour); the hook writer stores it
in the new token_usage_hourly table (MAX per hour) and the server
back-fills existing sessions at start. The daily split books each total
over its hourly rows' local days and the rest to the start date, so the
days still add up to the total.
This commit is contained in:
2026-10-05 08:03:05 +07:00
parent ed1bf20e6b
commit 65fe680d38
11 changed files with 478 additions and 25 deletions
+44
View File
@@ -219,6 +219,29 @@ db.exec(`
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
-- The same buckets split by the UTC hour (YYYY-MM-DDTHH) the tokens were
-- spent in, read from each transcript message's timestamp. Only the daily
-- cost split reads it (GET /api/pricing/cost): token_usage stays the total,
-- and whatever it holds beyond these rows is booked to the session's start.
CREATE TABLE IF NOT EXISTS token_usage_hourly (
session_id TEXT NOT NULL,
hour TEXT NOT NULL,
model TEXT NOT NULL,
speed TEXT NOT NULL DEFAULT 'standard',
inference_geo TEXT NOT NULL DEFAULT 'global',
service_tier TEXT NOT NULL DEFAULT 'standard',
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
cache_write_1h_tokens INTEGER NOT NULL DEFAULT 0,
web_search_requests INTEGER NOT NULL DEFAULT 0,
web_fetch_requests INTEGER NOT NULL DEFAULT 0,
code_execution_requests INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (session_id, hour, model, speed, inference_geo, service_tier),
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS model_pricing (
model_pattern TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
@@ -1774,6 +1797,27 @@ const stmts = {
baseline_cache_write = 0, baseline_cache_write_1h = 0,
baseline_web_search = 0, baseline_web_fetch = 0, baseline_code_execution = 0
`),
// One hour of one bucket, from a whole-transcript parse. MAX, not replace:
// a transcript that shrank (rewritten) must not take back an hour already
// counted. Args: session_id, hour, model, speed, inference_geo, service_tier,
// input, output, cache_read, cache_write, cache_write_1h, web_search,
// web_fetch, code_execution.
upsertHourlyUsage: db.prepare(`
INSERT INTO token_usage_hourly (session_id, hour, model, speed, inference_geo, service_tier,
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
cache_write_1h_tokens, web_search_requests, web_fetch_requests,
code_execution_requests)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(session_id, hour, model, speed, inference_geo, service_tier) DO UPDATE SET
input_tokens = MAX(input_tokens, excluded.input_tokens),
output_tokens = MAX(output_tokens, excluded.output_tokens),
cache_read_tokens = MAX(cache_read_tokens, excluded.cache_read_tokens),
cache_write_tokens = MAX(cache_write_tokens, excluded.cache_write_tokens),
cache_write_1h_tokens = MAX(cache_write_1h_tokens, excluded.cache_write_1h_tokens),
web_search_requests = MAX(web_search_requests, excluded.web_search_requests),
web_fetch_requests = MAX(web_fetch_requests, excluded.web_fetch_requests),
code_execution_requests = MAX(code_execution_requests, excluded.code_execution_requests)
`),
getTokenTotals: db.prepare(`
SELECT
COALESCE(SUM(input_tokens + baseline_input), 0) as total_input,