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:07:37 +07:00
committed by decolua
parent 960f8a0379
commit 54e3245ace
17 changed files with 558 additions and 71 deletions

View File

@@ -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>

View File

@@ -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">

View File

@@ -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" },
];

View File

@@ -126,6 +126,8 @@ export async function POST(request) {
let providerSpecificData = normalizeProviderSpecificData(provider, body, body.providerSpecificData);
// Compatible LLM nodes support multiple API-key connections (key pool); runtime
// rotates/fails over via getProviderCredentials. Embedding nodes stay single-connection.
if (isOpenAICompatibleProvider(provider)) {
const node = await getProviderNodeById(provider);
if (!node) {

View File

@@ -51,10 +51,11 @@ function getLocalDateKey(timestamp) {
}
function addToCounter(target, key, values) {
if (!target[key]) target[key] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0 };
if (!target[key]) target[key] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0 };
target[key].requests += values.requests || 1;
target[key].promptTokens += values.promptTokens || 0;
target[key].completionTokens += values.completionTokens || 0;
target[key].cachedTokens += values.cachedTokens || 0;
target[key].cost += values.cost || 0;
if (values.meta) Object.assign(target[key], values.meta);
}
@@ -62,12 +63,14 @@ function addToCounter(target, key, values) {
function aggregateEntryToDay(day, entry) {
const promptTokens = entry.tokens?.prompt_tokens || entry.tokens?.input_tokens || 0;
const completionTokens = entry.tokens?.completion_tokens || entry.tokens?.output_tokens || 0;
const cachedTokens = entry.tokens?.cached_tokens || entry.tokens?.cache_read_input_tokens || 0;
const cost = entry.cost || 0;
const vals = { promptTokens, completionTokens, cost };
const vals = { promptTokens, completionTokens, cachedTokens, cost };
day.requests = (day.requests || 0) + 1;
day.promptTokens = (day.promptTokens || 0) + promptTokens;
day.completionTokens = (day.completionTokens || 0) + completionTokens;
day.cachedTokens = (day.cachedTokens || 0) + cachedTokens;
day.cost = (day.cost || 0) + cost;
day.byProvider ||= {};
@@ -135,33 +138,11 @@ async function calculateCost(provider, model, tokens) {
const pricing = await getPricingForModel(provider, model);
if (!pricing) return 0;
let cost = 0;
const inputTokens = tokens.prompt_tokens || tokens.input_tokens || 0;
const cachedTokens = tokens.cached_tokens || tokens.cache_read_input_tokens || 0;
const nonCachedInput = Math.max(0, inputTokens - cachedTokens);
cost += nonCachedInput * (pricing.input / 1000000);
if (cachedTokens > 0) {
const cachedRate = pricing.cached || pricing.input;
cost += cachedTokens * (cachedRate / 1000000);
}
const outputTokens = tokens.completion_tokens || tokens.output_tokens || 0;
cost += outputTokens * (pricing.output / 1000000);
const reasoningTokens = tokens.reasoning_tokens || 0;
if (reasoningTokens > 0) {
const rate = pricing.reasoning || pricing.output;
cost += reasoningTokens * (rate / 1000000);
}
const cacheCreationTokens = tokens.cache_creation_input_tokens || 0;
if (cacheCreationTokens > 0) {
const rate = pricing.cache_creation || pricing.input;
cost += cacheCreationTokens * (rate / 1000000);
}
return cost;
// Delegate the actual math to the single source of truth (avoids the two
// copies drifting apart — see open-sse/providers/pricing.js for the
// cache-inclusive prompt_tokens convention this assumes).
const { calculateCostFromTokens } = await import("open-sse/providers/pricing.js");
return calculateCostFromTokens(tokens, pricing);
} catch (e) {
console.error("Error calculating cost:", e);
return 0;
@@ -398,6 +379,7 @@ export async function getUsageStats(period = "all") {
timestamp: r.timestamp, model: r.model, provider: r.provider || "",
promptTokens: t.prompt_tokens || t.input_tokens || 0,
completionTokens: t.completion_tokens || t.output_tokens || 0,
cachedTokens: t.cached_tokens || t.cache_read_input_tokens || 0,
status: r.status || "ok",
};
})
@@ -413,7 +395,7 @@ export async function getUsageStats(period = "all") {
const stats = {
totalRequests: 0,
totalPromptTokens: 0, totalCompletionTokens: 0, totalCost: 0,
totalPromptTokens: 0, totalCompletionTokens: 0, totalCachedTokens: 0, totalCost: 0,
byProvider: {}, byModel: {}, byAccount: {}, byApiKey: {}, byEndpoint: {},
last10Minutes: [],
pending: pendingRequests,
@@ -474,13 +456,15 @@ export async function getUsageStats(period = "all") {
const day = parseJson(dr.data, {});
stats.totalPromptTokens += day.promptTokens || 0;
stats.totalCompletionTokens += day.completionTokens || 0;
stats.totalCachedTokens += day.cachedTokens || 0;
stats.totalCost += day.cost || 0;
for (const [prov, p] of Object.entries(day.byProvider || {})) {
if (!stats.byProvider[prov]) stats.byProvider[prov] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0 };
if (!stats.byProvider[prov]) stats.byProvider[prov] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0 };
stats.byProvider[prov].requests += p.requests || 0;
stats.byProvider[prov].promptTokens += p.promptTokens || 0;
stats.byProvider[prov].completionTokens += p.completionTokens || 0;
stats.byProvider[prov].cachedTokens += p.cachedTokens || 0;
stats.byProvider[prov].cost += p.cost || 0;
}
@@ -490,11 +474,12 @@ export async function getUsageStats(period = "all") {
const statsKey = provider ? `${rawModel} (${provider})` : rawModel;
const providerDisplayName = providerNodeNameMap[provider] || provider;
if (!stats.byModel[statsKey]) {
stats.byModel[statsKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0, rawModel, provider: providerDisplayName, lastUsed: dateKey };
stats.byModel[statsKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel, provider: providerDisplayName, lastUsed: dateKey };
}
stats.byModel[statsKey].requests += m.requests || 0;
stats.byModel[statsKey].promptTokens += m.promptTokens || 0;
stats.byModel[statsKey].completionTokens += m.completionTokens || 0;
stats.byModel[statsKey].cachedTokens += m.cachedTokens || 0;
stats.byModel[statsKey].cost += m.cost || 0;
if (dateKey > (stats.byModel[statsKey].lastUsed || "")) stats.byModel[statsKey].lastUsed = dateKey;
}
@@ -506,11 +491,12 @@ export async function getUsageStats(period = "all") {
const providerDisplayName = providerNodeNameMap[provider] || provider;
const accountKey = `${rawModel} (${provider} - ${accountName})`;
if (!stats.byAccount[accountKey]) {
stats.byAccount[accountKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0, rawModel, provider: providerDisplayName, connectionId: connId, accountName, lastUsed: dateKey };
stats.byAccount[accountKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel, provider: providerDisplayName, connectionId: connId, accountName, lastUsed: dateKey };
}
stats.byAccount[accountKey].requests += a.requests || 0;
stats.byAccount[accountKey].promptTokens += a.promptTokens || 0;
stats.byAccount[accountKey].completionTokens += a.completionTokens || 0;
stats.byAccount[accountKey].cachedTokens += a.cachedTokens || 0;
stats.byAccount[accountKey].cost += a.cost || 0;
if (dateKey > (stats.byAccount[accountKey].lastUsed || "")) stats.byAccount[accountKey].lastUsed = dateKey;
}
@@ -525,11 +511,12 @@ export async function getUsageStats(period = "all") {
const apiKeyMasked = maskApiKey(apiKeyVal);
const apiKeyKey = apiKeyMasked || "local-no-key";
if (!stats.byApiKey[akKey]) {
stats.byApiKey[akKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0, rawModel, provider: providerDisplayName, apiKeyMasked, keyName, apiKeyKey, lastUsed: dateKey };
stats.byApiKey[akKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel, provider: providerDisplayName, apiKeyMasked, keyName, apiKeyKey, lastUsed: dateKey };
}
stats.byApiKey[akKey].requests += ak.requests || 0;
stats.byApiKey[akKey].promptTokens += ak.promptTokens || 0;
stats.byApiKey[akKey].completionTokens += ak.completionTokens || 0;
stats.byApiKey[akKey].cachedTokens += ak.cachedTokens || 0;
stats.byApiKey[akKey].cost += ak.cost || 0;
if (dateKey > (stats.byApiKey[akKey].lastUsed || "")) stats.byApiKey[akKey].lastUsed = dateKey;
}
@@ -540,11 +527,12 @@ export async function getUsageStats(period = "all") {
const provider = ep.provider || "";
const providerDisplayName = providerNodeNameMap[provider] || provider;
if (!stats.byEndpoint[epKey]) {
stats.byEndpoint[epKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0, endpoint, rawModel, provider: providerDisplayName, lastUsed: dateKey };
stats.byEndpoint[epKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, endpoint, rawModel, provider: providerDisplayName, lastUsed: dateKey };
}
stats.byEndpoint[epKey].requests += ep.requests || 0;
stats.byEndpoint[epKey].promptTokens += ep.promptTokens || 0;
stats.byEndpoint[epKey].completionTokens += ep.completionTokens || 0;
stats.byEndpoint[epKey].cachedTokens += ep.cachedTokens || 0;
stats.byEndpoint[epKey].cost += ep.cost || 0;
if (dateKey > (stats.byEndpoint[epKey].lastUsed || "")) stats.byEndpoint[epKey].lastUsed = dateKey;
}
@@ -595,26 +583,30 @@ export async function getUsageStats(period = "all") {
const tokens = parseJson(r.tokens, {}) || {};
const promptTokens = tokens.prompt_tokens || 0;
const completionTokens = tokens.completion_tokens || 0;
const cachedTokens = tokens.cached_tokens || tokens.cache_read_input_tokens || 0;
const entryCost = r.cost || 0;
const providerDisplayName = providerNodeNameMap[r.provider] || r.provider;
stats.totalPromptTokens += promptTokens;
stats.totalCompletionTokens += completionTokens;
stats.totalCachedTokens += cachedTokens;
stats.totalCost += entryCost;
if (!stats.byProvider[r.provider]) stats.byProvider[r.provider] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0 };
if (!stats.byProvider[r.provider]) stats.byProvider[r.provider] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0 };
stats.byProvider[r.provider].requests++;
stats.byProvider[r.provider].promptTokens += promptTokens;
stats.byProvider[r.provider].completionTokens += completionTokens;
stats.byProvider[r.provider].cachedTokens += cachedTokens;
stats.byProvider[r.provider].cost += entryCost;
const modelKey = r.provider ? `${r.model} (${r.provider})` : r.model;
if (!stats.byModel[modelKey]) {
stats.byModel[modelKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, lastUsed: r.timestamp };
stats.byModel[modelKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, lastUsed: r.timestamp };
}
stats.byModel[modelKey].requests++;
stats.byModel[modelKey].promptTokens += promptTokens;
stats.byModel[modelKey].completionTokens += completionTokens;
stats.byModel[modelKey].cachedTokens += cachedTokens;
stats.byModel[modelKey].cost += entryCost;
if (new Date(r.timestamp) > new Date(stats.byModel[modelKey].lastUsed)) stats.byModel[modelKey].lastUsed = r.timestamp;
@@ -622,11 +614,12 @@ export async function getUsageStats(period = "all") {
const accountName = connectionMap[r.connectionId] || `Account ${r.connectionId.slice(0, 8)}...`;
const accountKey = `${r.model} (${r.provider} - ${accountName})`;
if (!stats.byAccount[accountKey]) {
stats.byAccount[accountKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, connectionId: r.connectionId, accountName, lastUsed: r.timestamp };
stats.byAccount[accountKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, connectionId: r.connectionId, accountName, lastUsed: r.timestamp };
}
stats.byAccount[accountKey].requests++;
stats.byAccount[accountKey].promptTokens += promptTokens;
stats.byAccount[accountKey].completionTokens += completionTokens;
stats.byAccount[accountKey].cachedTokens += cachedTokens;
stats.byAccount[accountKey].cost += entryCost;
if (new Date(r.timestamp) > new Date(stats.byAccount[accountKey].lastUsed)) stats.byAccount[accountKey].lastUsed = r.timestamp;
}
@@ -637,27 +630,27 @@ export async function getUsageStats(period = "all") {
const apiKeyMasked = maskApiKey(r.apiKey);
const akKey = `${apiKeyMasked}|${r.model}|${r.provider || "unknown"}`;
if (!stats.byApiKey[akKey]) {
stats.byApiKey[akKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, apiKeyMasked, keyName, apiKeyKey: apiKeyMasked, lastUsed: r.timestamp };
stats.byApiKey[akKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, apiKeyMasked, keyName, apiKeyKey: apiKeyMasked, lastUsed: r.timestamp };
}
const ake = stats.byApiKey[akKey];
ake.requests++; ake.promptTokens += promptTokens; ake.completionTokens += completionTokens; ake.cost += entryCost;
ake.requests++; ake.promptTokens += promptTokens; ake.completionTokens += completionTokens; ake.cachedTokens += cachedTokens; ake.cost += entryCost;
if (new Date(r.timestamp) > new Date(ake.lastUsed)) ake.lastUsed = r.timestamp;
} else {
if (!stats.byApiKey["local-no-key"]) {
stats.byApiKey["local-no-key"] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, apiKeyMasked: null, keyName: "Local (No API Key)", apiKeyKey: "local-no-key", lastUsed: r.timestamp };
stats.byApiKey["local-no-key"] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, rawModel: r.model, provider: providerDisplayName, apiKeyMasked: null, keyName: "Local (No API Key)", apiKeyKey: "local-no-key", lastUsed: r.timestamp };
}
const ake = stats.byApiKey["local-no-key"];
ake.requests++; ake.promptTokens += promptTokens; ake.completionTokens += completionTokens; ake.cost += entryCost;
ake.requests++; ake.promptTokens += promptTokens; ake.completionTokens += completionTokens; ake.cachedTokens += cachedTokens; ake.cost += entryCost;
if (new Date(r.timestamp) > new Date(ake.lastUsed)) ake.lastUsed = r.timestamp;
}
const endpoint = r.endpoint || "Unknown";
const epKey = `${endpoint}|${r.model}|${r.provider || "unknown"}`;
if (!stats.byEndpoint[epKey]) {
stats.byEndpoint[epKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0, endpoint, rawModel: r.model, provider: providerDisplayName, lastUsed: r.timestamp };
stats.byEndpoint[epKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0, endpoint, rawModel: r.model, provider: providerDisplayName, lastUsed: r.timestamp };
}
const epe = stats.byEndpoint[epKey];
epe.requests++; epe.promptTokens += promptTokens; epe.completionTokens += completionTokens; epe.cost += entryCost;
epe.requests++; epe.promptTokens += promptTokens; epe.completionTokens += completionTokens; epe.cachedTokens += cachedTokens; epe.cost += entryCost;
if (new Date(r.timestamp) > new Date(epe.lastUsed)) epe.lastUsed = r.timestamp;
}
}

View File

@@ -89,9 +89,16 @@ function sortData(dataMap, pendingMap = {}, sortBy, sortOrder) {
.map(([key, data]) => {
const totalTokens = (data.promptTokens || 0) + (data.completionTokens || 0);
const totalCost = data.cost || 0;
const inputCost = totalTokens > 0 ? (data.promptTokens || 0) * (totalCost / totalTokens) : 0;
// ponytail: cost split is a token-share allocation of the (rate-accurate)
// server total, not a per-rate recompute. cached is a subset of prompt, so
// peel it out of the input share. Upgrade to a stored per-component cost
// breakdown if exact cached-rate cost display is needed.
const cachedTokens = data.cachedTokens || 0;
const nonCachedInput = Math.max(0, (data.promptTokens || 0) - cachedTokens);
const inputCost = totalTokens > 0 ? nonCachedInput * (totalCost / totalTokens) : 0;
const cachedCost = totalTokens > 0 ? cachedTokens * (totalCost / totalTokens) : 0;
const outputCost = totalTokens > 0 ? (data.completionTokens || 0) * (totalCost / totalTokens) : 0;
return { ...data, key, totalTokens, totalCost, inputCost, outputCost, pending: pendingMap[key] || 0 };
return { ...data, key, totalTokens, totalCost, inputCost, cachedCost, outputCost, pending: pendingMap[key] || 0 };
})
.sort((a, b) => {
let valA = a[sortBy];
@@ -122,7 +129,7 @@ function groupDataByKey(data, keyField) {
if (!groups[gk]) {
groups[gk] = {
groupKey: gk,
summary: { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cost: 0, inputCost: 0, outputCost: 0, lastUsed: null, pending: 0 },
summary: { requests: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, totalTokens: 0, cost: 0, inputCost: 0, cachedCost: 0, outputCost: 0, lastUsed: null, pending: 0 },
items: [],
};
}
@@ -130,9 +137,11 @@ function groupDataByKey(data, keyField) {
s.requests += item.requests || 0;
s.promptTokens += item.promptTokens || 0;
s.completionTokens += item.completionTokens || 0;
s.cachedTokens += item.cachedTokens || 0;
s.totalTokens += item.totalTokens || 0;
s.cost += item.cost || 0;
s.inputCost += item.inputCost || 0;
s.cachedCost += item.cachedCost || 0;
s.outputCost += item.outputCost || 0;
s.pending += item.pending || 0;
if (item.lastUsed && (!s.lastUsed || new Date(item.lastUsed) > new Date(s.lastUsed))) {