Files
AgentHub/server/lib/hourly-usage.js
T
nntrivi2001 65fe680d38 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.
2026-10-05 08:03:05 +07:00

73 lines
2.4 KiB
JavaScript

/**
* @file Per-hour token usage (`token_usage_hourly`): written from a Claude
* transcript's `tokensByHour` (`server/lib/transcript-cache.js`) by the hook
* writer, back-filled once for sessions recorded before the table existed, and
* read only by the daily cost split in `GET /api/pricing/cost` — so a session
* that runs for days books each day's spend to that day instead of its start.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { db, stmts } = require("../db");
const TranscriptCache = require("./transcript-cache");
/** Write every hour of one transcript parse (MAX per hour, see the statement). */
function writeHourly(sessionId, tokensByHour) {
// ponytail: rewrites every hour of the session on each hook — ~1 row per
// active hour per model, cheap; write only the newest hours if it ever shows.
for (const t of Object.values(tokensByHour || {})) {
stmts.upsertHourlyUsage.run(
sessionId,
t.hour,
t.model,
t.speed,
t.geo,
t.tier,
t.input,
t.output,
t.cacheRead,
t.cacheWrite,
t.cacheWrite1h,
t.webSearch,
t.webFetch,
t.codeExec
);
}
}
/**
* Fill the table for Claude sessions that have tokens but no hourly rows yet,
* one transcript per tick so a large history never blocks the event loop.
* Resolves with the number of sessions written.
*/
function backfillHourly() {
const rows = db
.prepare(
`SELECT s.id, s.transcript_path FROM sessions s
WHERE s.transcript_path IS NOT NULL AND s.transcript_path != ''
AND EXISTS (SELECT 1 FROM token_usage tu WHERE tu.session_id = s.id)
AND NOT EXISTS (SELECT 1 FROM token_usage_hourly h WHERE h.session_id = s.id)`
)
.all();
const cache = new TranscriptCache(1);
let written = 0;
return new Promise((resolve) => {
const next = (i) => {
if (i >= rows.length) return resolve(written);
const { id, transcript_path } = rows[i];
try {
const result = cache.extract(transcript_path);
if (result?.tokensByHour && Object.keys(result.tokensByHour).length > 0) {
db.transaction(() => writeHourly(id, result.tokensByHour))();
written++;
}
} catch {
/* an unreadable transcript keeps its start-date booking */
}
setImmediate(() => next(i + 1));
};
next(0);
});
}
module.exports = { writeHourly, backfillHourly };