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
+28 -7
View File
@@ -18,7 +18,12 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useRequestLogs } from "@/lib/query/usage";
import type { LogFilters, UsageRangeSelection } from "@/types/usage";
import {
KNOWN_APP_TYPES,
getFreshInputTokens,
type LogFilters,
type UsageRangeSelection,
} from "@/types/usage";
import { ChevronLeft, ChevronRight, Search, X } from "lucide-react";
import { UsageDateRangePicker } from "./UsageDateRangePicker";
import {
@@ -138,9 +143,11 @@ export function RequestLogTable({
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t("usage.allApps")}</SelectItem>
<SelectItem value="claude">Claude</SelectItem>
<SelectItem value="codex">Codex</SelectItem>
<SelectItem value="gemini">Gemini</SelectItem>
{KNOWN_APP_TYPES.map((at) => (
<SelectItem key={at} value={at}>
{t(`usage.appFilter.${at}`, { defaultValue: at })}
</SelectItem>
))}
</SelectContent>
</Select>
@@ -323,9 +330,23 @@ export function RequestLogTable({
</div>
</TableCell>
<TableCell className="text-center px-1.5">
<div className="tabular-nums">
{fmtInt(log.inputTokens, locale)}
</div>
{(() => {
const freshInput = getFreshInputTokens(log);
const isCacheInclusive =
log.inputTokens !== freshInput;
return (
<div
className="tabular-nums"
title={
isCacheInclusive
? `Raw: ${log.inputTokens.toLocaleString()}`
: undefined
}
>
{fmtInt(freshInput, locale)}
</div>
);
})()}
{(log.cacheReadTokens > 0 ||
log.cacheCreationTokens > 0) && (
<div className="text-[10px] text-muted-foreground whitespace-nowrap">