feat: add cache-aware token usage dashboard (#1338)

* feat: add cache-aware token usage dashboard

Track normalized Claude, Codex, and ACP usage with incremental SQLite backfill. Exclude imported transcript history, rebuild usage after history rewrites, and expose an owner-only dashboard with cache-aware totals and breakdowns.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix: preserve usage model and local dates

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix: normalize cached usage and timezone buckets

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

---------

Co-authored-by: HAPI <noreply@hapi.run>
This commit is contained in:
2026-08-03 18:02:26 +08:00
committed by GitHub
co-authored by HAPI
parent 518bd7a9a9
commit 1761b696f7
49 changed files with 1904 additions and 46 deletions
+30 -2
View File
@@ -36,6 +36,16 @@ function extractTurnContextReasoningEffort(event: CodexSessionEvent): ReasoningE
return effort.trim().toLowerCase();
}
function extractTurnContextModel(event: CodexSessionEvent): string | null | undefined {
if (event.type !== 'turn_context' || !event.payload || typeof event.payload !== 'object') {
return undefined;
}
const model = (event.payload as Record<string, unknown>).model;
if (model === null) return null;
if (typeof model !== 'string' || !model.trim()) return undefined;
return model.trim();
}
export async function codexLocalLauncher(session: CodexSession): Promise<'switch' | 'exit'> {
const resumeSessionId = session.sessionId;
let primarySessionId = resumeSessionId;
@@ -47,6 +57,7 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
let transcriptLocator: CodexTranscriptLocator | null = null;
let scannerTranscriptPath: string | null = null;
let scannerReplayedExistingHistory = false;
let transcriptModel: string | null = null;
const pendingPlansByTurnId = new Map<string, ProposedPlanMessage>();
const pendingExecWrappers = new Map<string, PendingExecWrapper>();
const toolHookBridge = new CodexToolHookBridge();
@@ -202,7 +213,11 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
}
session.onSessionFound(sessionId);
},
onEvent: (event) => {
onEvent: (event, context) => {
const observedModel = extractTurnContextModel(event);
if (observedModel !== undefined) {
transcriptModel = observedModel;
}
const observedReasoningEffort = extractTurnContextReasoningEffort(event);
if (observedReasoningEffort !== undefined) {
session.setModelReasoningEffort(observedReasoningEffort);
@@ -242,7 +257,20 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
flushPendingExecWrapper(message.callId, message);
}
} else {
session.sendAgentMessage(message);
const scopedMessage = message.type !== 'token_count'
? message
: context.replayedHistory
? { ...message, model: transcriptModel, hapiUsageScope: 'imported-history' }
: primarySessionId
? {
...message,
model: transcriptModel,
threadId: primarySessionId,
thread_id: primarySessionId,
hapiUsageScope: 'managed'
}
: { ...message, model: transcriptModel };
session.sendAgentMessage(scopedMessage);
}
}
if (converted?.finishedTurnId) {