Surface cache_creation_input_tokens in usage (token-cost diagnostics)

Decode cache_creation_input_tokens from the result event and surface it
alongside cache_read in the spike console and the app transcript row, so a
session's prompt-cache write/read split is visible. Cache writes (billed at a
premium) were previously invisible, making cache-thrash impossible to diagnose
from the UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-06-14 17:16:13 -07:00
co-authored by Claude Opus 4.8
parent 48c9ffadfd
commit 763f5facee
4 changed files with 12 additions and 2 deletions
+2
View File
@@ -189,6 +189,8 @@ struct TranscriptRow: View {
private func usageText(_ usage: Usage) -> String {
var parts: [String] = []
if let i = usage.inputTokens { parts.append("in \(i)") }
if let r = usage.cachedInputTokens, r > 0 { parts.append("cache-read \(r)") }
if let w = usage.cacheCreationInputTokens, w > 0 { parts.append("cache-write \(w)") }
if let o = usage.outputTokens { parts.append("out \(o)") }
if let cost = usage.costUSD { parts.append(String(format: "$%.4f", cost)) }
return parts.joined(separator: " · ")
@@ -308,6 +308,7 @@ public final class ClaudeStreamDecoder {
usage = Usage(
inputTokens: usageObject["input_tokens"]?.intValue,
cachedInputTokens: usageObject["cache_read_input_tokens"]?.intValue,
cacheCreationInputTokens: usageObject["cache_creation_input_tokens"]?.intValue,
outputTokens: usageObject["output_tokens"]?.intValue,
reasoningTokens: nil,
costUSD: root["total_cost_usd"]?.numberValue)
+8 -1
View File
@@ -182,16 +182,23 @@ public struct FileChange: Sendable, Codable, Equatable {
public struct Usage: Sendable, Codable, Equatable {
public let inputTokens: Int?
public let cachedInputTokens: Int?
/// Tokens written to the prompt cache this turn (`cache_creation_input_tokens`).
/// Billed at a premium; a healthy multi-turn session writes the stable prefix
/// once and then reads it. A large value on *every* turn is the signature of a
/// prefix that keeps changing (cache thrash) see token-cost diagnostics.
public let cacheCreationInputTokens: Int?
public let outputTokens: Int?
public let reasoningTokens: Int?
public let costUSD: Double?
public init(
inputTokens: Int? = nil, cachedInputTokens: Int? = nil, outputTokens: Int? = nil,
inputTokens: Int? = nil, cachedInputTokens: Int? = nil,
cacheCreationInputTokens: Int? = nil, outputTokens: Int? = nil,
reasoningTokens: Int? = nil, costUSD: Double? = nil
) {
self.inputTokens = inputTokens
self.cachedInputTokens = cachedInputTokens
self.cacheCreationInputTokens = cacheCreationInputTokens
self.outputTokens = outputTokens
self.reasoningTokens = reasoningTokens
self.costUSD = costUSD
+1 -1
View File
@@ -189,7 +189,7 @@ struct Spike {
print("\(prefix) ▶ approval resolved by \(resolved.decidedBy)")
case .usage(let usage):
let cost = usage.costUSD.map { String(format: "$%.4f", $0) } ?? "n/a"
print("\(prefix) 📊 in=\(usage.inputTokens ?? 0) cached=\(usage.cachedInputTokens ?? 0) out=\(usage.outputTokens ?? 0) cost=\(cost)")
print("\(prefix) 📊 in=\(usage.inputTokens ?? 0) cacheRead=\(usage.cachedInputTokens ?? 0) cacheWrite=\(usage.cacheCreationInputTokens ?? 0) out=\(usage.outputTokens ?? 0) cost=\(cost)")
case .rateLimit(let rl):
let resets = rl.resetsAt.map { " resets=\($0)" } ?? ""
print("\(prefix) ⏳ rate-limit \(rl.rateLimitType ?? "?") status=\(rl.status ?? "?")\(resets)")