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:
@@ -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))) {
|
||||
|
||||
Reference in New Issue
Block a user