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

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