mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
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:
committed by
decolua
co-authored by
Cursor
parent
960f8a0379
commit
54e3245ace
@@ -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" },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user