fix(coding-agent): exclude cacheRead from token display, add per-subagent cost
Token counter (token_total status-line segment, subagent progress tree, session-observer stats line) previously included cacheRead in its cumulative sum. With Anthropic prompt caching, cacheRead per turn equals the full cached context, so summing across N turns gives N*context_size -- a session with a 1M context and 5 turns showed ~5M tokens despite no compaction occurring. Fix: display shows input + output + cacheWrite per turn. cacheWrite is kept because each byte is written once; cacheRead re-reads the same context every turn. Dedicated cache_read/cache_write status-line segments still show cache activity; billing cost is unaffected. Also adds per-subagent cost display (dollar amount, statusLineCost color) accumulated incrementally from message_end events. Hidden when cost is zero (subscription/OAuth providers). Brings token and cost display in line with what Claude Code shows per-agent.
This commit is contained in:
@@ -379,21 +379,29 @@ function firstNumberField(record: Record<string, unknown>, keys: string[]): numb
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize usage objects from different event formats.
|
||||
* Tokens for progress display: input + output + cacheWrite per turn.
|
||||
*
|
||||
* Deliberately excludes cacheRead. With prompt caching, cacheRead in each turn
|
||||
* equals the full cached context (potentially hundreds of KB), so summing it
|
||||
* across all turns produces a cumulative total that is N×context_size — far
|
||||
* larger than the context window and misleading as a "work done" metric.
|
||||
* cacheWrite is kept because each byte is written once, not repeated per turn.
|
||||
* The cost segment handles billing; dedicated cache_read/cache_write segments
|
||||
* handle cache-specific monitoring.
|
||||
*/
|
||||
function getUsageTokens(usage: unknown): number {
|
||||
if (!usage || typeof usage !== "object") return 0;
|
||||
const record = usage as Record<string, unknown>;
|
||||
|
||||
const totalTokens = firstNumberField(record, ["totalTokens", "total_tokens"]);
|
||||
if (totalTokens !== undefined && totalTokens > 0) return totalTokens;
|
||||
|
||||
const input = firstNumberField(record, ["input", "input_tokens", "inputTokens"]) ?? 0;
|
||||
const output = firstNumberField(record, ["output", "output_tokens", "outputTokens"]) ?? 0;
|
||||
const cacheRead = firstNumberField(record, ["cacheRead", "cache_read", "cacheReadTokens"]) ?? 0;
|
||||
const cacheWrite = firstNumberField(record, ["cacheWrite", "cache_write", "cacheWriteTokens"]) ?? 0;
|
||||
|
||||
return input + output + cacheRead + cacheWrite;
|
||||
const computed = input + output + cacheWrite;
|
||||
if (computed > 0) return computed;
|
||||
// Fallback for providers that only surface a pre-summed total without individual
|
||||
// field breakdown. This total includes cacheRead, but returning it is still better
|
||||
// than silently showing 0 for those providers.
|
||||
return firstNumberField(record, ["totalTokens", "total_tokens"]) ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -497,6 +505,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
recentOutput: [],
|
||||
toolCount: 0,
|
||||
tokens: 0,
|
||||
cost: 0,
|
||||
durationMs: 0,
|
||||
modelOverride,
|
||||
};
|
||||
@@ -892,6 +901,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
accumulatedUsage.cost.cacheRead += getNumberField(costRecord, "cacheRead") ?? 0;
|
||||
accumulatedUsage.cost.cacheWrite += getNumberField(costRecord, "cacheWrite") ?? 0;
|
||||
accumulatedUsage.cost.total += getNumberField(costRecord, "total") ?? 0;
|
||||
progress.cost = accumulatedUsage.cost.total;
|
||||
}
|
||||
}
|
||||
// Accumulate tokens for progress display
|
||||
|
||||
Reference in New Issue
Block a user