feat(usage): filter-driven Hero with cache-normalized totals

- Normalize OpenAI/Gemini input_tokens semantics in SQL via the new
  fresh_input_sql helper (cache_read subtracted at query time, no data
  migration). Recovers correct cache hit rates for Codex/Gemini.
- Add get_usage_summary_by_app endpoint for per-app split (single
  UNION ALL + GROUP BY, avoids N+1).
- Replace UsageSummaryCards + AppBreakdownRail with a single
  filter-driven UsageHero card; clicking a filter button now truly
  changes the displayed numbers and the title accent color.
- Tighten KNOWN_APP_TYPES to the 3 app_types whose token data is
  reliably collected (claude/codex/gemini); hide claude-desktop,
  hermes, opencode, openclaw filter buttons and i18n keys.
- Flag cache_creation as N/A for OpenAI-style protocols (Codex,
  Gemini); show a "partial" tooltip when the All view mixes both
  protocol families.
This commit is contained in:
Jason
2026-05-13 10:27:29 +08:00
parent c12364a940
commit edf28b6422
17 changed files with 931 additions and 219 deletions
+65 -1
View File
@@ -71,6 +71,15 @@ export interface UsageSummary {
totalCacheCreationTokens: number;
totalCacheReadTokens: number;
successRate: number;
/** input + output + cache_creation + cache_read, all cache-normalized */
realTotalTokens: number;
/** cache_read / (input + cache_creation + cache_read), range 01 */
cacheHitRate: number;
}
export interface UsageSummaryByApp {
appType: string;
summary: UsageSummary;
}
export interface DailyStats {
@@ -129,7 +138,62 @@ export interface UsageRangeSelection {
customEndDate?: number;
}
export type AppTypeFilter = "all" | "claude" | "codex" | "gemini";
/**
* App types whose token usage is reliably collected by the proxy.
*
* The proxy has handlers for `claude-desktop` too, but in practice those
* requests overwhelmingly fail (500/503) and contribute near-zero tokens, so
* it is hidden from the Dashboard. `opencode` / `openclaw` / `hermes` have
* no proxy handler at all — they appear only as managed apps elsewhere.
*/
export type AppType = "claude" | "codex" | "gemini";
export type AppTypeFilter = "all" | AppType;
export const KNOWN_APP_TYPES: ReadonlyArray<AppType> = [
"claude",
"codex",
"gemini",
];
/**
* App types whose proxy uses an OpenAI-style protocol. Two consequences:
*
* 1. `inputTokens` already includes the cached portion (must subtract
* `cacheReadTokens` to get fresh-input semantics — see
* [getFreshInputTokens]).
* 2. The protocol does not report cache _creation_ separately, only cache
* _reads_. So `cacheCreationTokens` is always 0 for these app types and
* the UI should label it as N/A rather than 0.
*
* Mirror of the Rust `CACHE_INCLUSIVE_APP_TYPES` whitelist.
*/
export const CACHE_INCLUSIVE_APP_TYPES: ReadonlySet<string> = new Set([
"codex",
"gemini",
]);
/** Subset of request-log fields needed to derive cache-normalized input. */
export interface CacheNormalizableLog {
appType: string;
inputTokens: number;
cacheReadTokens: number;
}
/**
* For a single request log, return the input token count with cache reads
* removed. Anthropic-style providers already report `inputTokens` without
* cache, so they pass through unchanged.
*/
export function getFreshInputTokens(log: CacheNormalizableLog): number {
if (
CACHE_INCLUSIVE_APP_TYPES.has(log.appType) &&
log.inputTokens >= log.cacheReadTokens
) {
return log.inputTokens - log.cacheReadTokens;
}
return log.inputTokens;
}
export interface StatsFilters {
timeRange: UsageRangePreset;