feat(usage): track cached tokens + correct input/output/cache cost (#2209)

Normalize every provider to one cache-inclusive convention via
canonicalizeUsage() before persist, and price cached + cache_creation as
subsets of prompt_tokens in calculateCostFromTokens() to stop
double-counting. usageRepo now delegates cost math to a single source.
Surface Cached tokens/cost across dashboard (overview, tokens, cost,
details). Merge Claude message_start cache with message_delta output so
cache counts survive. Compatible LLM nodes now allow multiple API-key
connections (key pool).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
hodtien
2026-07-03 15:18:27 +07:00
committed by decolua
co-authored by Cursor
parent 960f8a0379
commit 54e3245ace
17 changed files with 558 additions and 71 deletions
@@ -8,7 +8,7 @@ const fmtCost = (n) => `$${(n || 0).toFixed(2)}`;
export default function OverviewCards({ stats }) {
return (
<div className="grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-4 sm:gap-4">
<div className="grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 sm:gap-4">
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
<span className="text-text-muted text-sm uppercase font-semibold">Total Requests</span>
<span className="truncate text-2xl font-bold">{fmt(stats.totalRequests)}</span>
@@ -17,6 +17,10 @@ export default function OverviewCards({ stats }) {
<span className="text-text-muted text-sm uppercase font-semibold">Total Input Tokens</span>
<span className="truncate text-2xl font-bold text-primary">{fmt(stats.totalPromptTokens)}</span>
</Card>
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
<span className="text-text-muted text-sm uppercase font-semibold">Cached Tokens</span>
<span className="truncate text-2xl font-bold text-info">{fmt(stats.totalCachedTokens)}</span>
</Card>
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
<span className="text-text-muted text-sm uppercase font-semibold">Output Tokens</span>
<span className="truncate text-2xl font-bold text-success">{fmt(stats.totalCompletionTokens)}</span>
@@ -82,9 +82,20 @@ function CollapsibleSection({ title, children, defaultOpen = false, icon = null
);
}
function getCachedTokens(tokens) {
return tokens?.cached_tokens || tokens?.cache_read_input_tokens || 0;
}
function getCacheCreationTokens(tokens) {
return tokens?.cache_creation_input_tokens || 0;
}
function getInputTokens(tokens) {
const prompt = tokens?.prompt_tokens || tokens?.input_tokens || 0;
const cache = tokens?.cached_tokens || tokens?.cache_read_input_tokens || 0;
// Canonical storage keeps prompt cache-inclusive. Legacy Claude rows may have
// stored prompt cache-exclusive; fall back to cache when it's larger so old
// rows don't under-report input.
const cache = getCachedTokens(tokens);
return prompt < cache ? cache : prompt;
}
@@ -245,6 +256,8 @@ export default function RequestDetailsTab() {
<th className="text-left p-4 text-sm font-semibold text-text-main">Model</th>
<th className="text-left p-4 text-sm font-semibold text-text-main">Provider</th>
<th className="text-right p-4 text-sm font-semibold text-text-main">Input Tokens</th>
<th className="text-right p-4 text-sm font-semibold text-text-main">Cached</th>
<th className="text-right p-4 text-sm font-semibold text-text-main">Cache Creation</th>
<th className="text-right p-4 text-sm font-semibold text-text-main">Output Tokens</th>
<th className="text-left p-4 text-sm font-semibold text-text-main">Latency</th>
<th className="text-center p-4 text-sm font-semibold text-text-main">Action</th>
@@ -286,6 +299,12 @@ export default function RequestDetailsTab() {
<td className="p-4 text-sm text-text-main text-right font-mono">
{getInputTokens(detail.tokens).toLocaleString()}
</td>
<td className="p-4 text-sm text-text-main text-right font-mono">
{getCachedTokens(detail.tokens) > 0 ? getCachedTokens(detail.tokens).toLocaleString() : "—"}
</td>
<td className="p-4 text-sm text-text-main text-right font-mono">
{getCacheCreationTokens(detail.tokens) > 0 ? getCacheCreationTokens(detail.tokens).toLocaleString() : "—"}
</td>
<td className="p-4 text-sm text-text-main text-right font-mono">
{detail.tokens?.completion_tokens?.toLocaleString() || 0}
</td>
@@ -370,6 +389,22 @@ export default function RequestDetailsTab() {
{getInputTokens(selectedDetail.tokens).toLocaleString()}
</span>
</div>
{getCachedTokens(selectedDetail.tokens) > 0 && (
<div>
<span className="text-text-muted">Cached Tokens:</span>{" "}
<span className="text-text-main font-mono">
{getCachedTokens(selectedDetail.tokens).toLocaleString()}
</span>
</div>
)}
{getCacheCreationTokens(selectedDetail.tokens) > 0 && (
<div>
<span className="text-text-muted">Cache Creation:</span>{" "}
<span className="text-text-main font-mono">
{getCacheCreationTokens(selectedDetail.tokens).toLocaleString()}
</span>
</div>
)}
<div>
<span className="text-text-muted">Output Tokens:</span>{" "}
<span className="text-text-main font-mono">
@@ -38,6 +38,9 @@ function ValueCells({ item, viewMode, isSummary = false }) {
<td className="px-6 py-3 text-right text-text-muted">
{isSummary && item.promptTokens === undefined ? "—" : fmt(item.promptTokens)}
</td>
<td className="px-6 py-3 text-right text-text-muted">
{item.cachedTokens ? fmt(item.cachedTokens) : "—"}
</td>
<td className="px-6 py-3 text-right text-text-muted">
{isSummary && item.completionTokens === undefined ? "—" : fmt(item.completionTokens)}
</td>
@@ -52,6 +55,9 @@ function ValueCells({ item, viewMode, isSummary = false }) {
<td className="px-6 py-3 text-right text-text-muted">
{isSummary && item.inputCost === undefined ? "—" : fmtCost(item.inputCost)}
</td>
<td className="px-6 py-3 text-right text-text-muted">
{item.cachedCost ? fmtCost(item.cachedCost) : "—"}
</td>
<td className="px-6 py-3 text-right text-text-muted">
{isSummary && item.outputCost === undefined ? "—" : fmtCost(item.outputCost)}
</td>
@@ -133,12 +139,14 @@ export default function UsageTable({
if (viewMode === "tokens") {
return [
{ field: "promptTokens", label: "Input Tokens" },
{ field: "cachedTokens", label: "Cached" },
{ field: "completionTokens", label: "Output Tokens" },
{ field: "totalTokens", label: "Total Tokens" },
];
}
return [
{ field: "promptTokens", label: "Input Cost" },
{ field: "cachedCost", label: "Cached Cost" },
{ field: "completionTokens", label: "Output Cost" },
{ field: "cost", label: "Total Cost" },
];