feat(usage): add support for detailed token breakdown in usage tracking

- Introduced `CacheReadTokens` and `CacheCreationTokens` to enhance token breakdown.
- Refactored `parseClaudeUsageNode` for cleaner and reusable logic.
- Adjusted helpers and updated token calculations to align with the new fields.
This commit is contained in:
Luis Pater
2026-05-12 11:59:07 +08:00
parent 041ccf0195
commit bd8c05a830
3 changed files with 37 additions and 31 deletions
+14 -10
View File
@@ -49,11 +49,13 @@ func (p *usageQueuePlugin) HandleUsage(ctx context.Context, record coreusage.Rec
requestID := strings.TrimSpace(internallogging.GetRequestID(ctx)) requestID := strings.TrimSpace(internallogging.GetRequestID(ctx))
tokens := tokenStats{ tokens := tokenStats{
InputTokens: record.Detail.InputTokens, InputTokens: record.Detail.InputTokens,
OutputTokens: record.Detail.OutputTokens, OutputTokens: record.Detail.OutputTokens,
ReasoningTokens: record.Detail.ReasoningTokens, ReasoningTokens: record.Detail.ReasoningTokens,
CachedTokens: record.Detail.CachedTokens, CachedTokens: record.Detail.CachedTokens,
TotalTokens: record.Detail.TotalTokens, CacheReadTokens: record.Detail.CacheReadTokens,
CacheCreationTokens: record.Detail.CacheCreationTokens,
TotalTokens: record.Detail.TotalTokens,
} }
if tokens.TotalTokens == 0 { if tokens.TotalTokens == 0 {
tokens.TotalTokens = tokens.InputTokens + tokens.OutputTokens + tokens.ReasoningTokens tokens.TotalTokens = tokens.InputTokens + tokens.OutputTokens + tokens.ReasoningTokens
@@ -116,11 +118,13 @@ type requestDetail struct {
} }
type tokenStats struct { type tokenStats struct {
InputTokens int64 `json:"input_tokens"` InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"` OutputTokens int64 `json:"output_tokens"`
ReasoningTokens int64 `json:"reasoning_tokens"` ReasoningTokens int64 `json:"reasoning_tokens"`
CachedTokens int64 `json:"cached_tokens"` CachedTokens int64 `json:"cached_tokens"`
TotalTokens int64 `json:"total_tokens"` CacheReadTokens int64 `json:"cache_read_tokens"`
CacheCreationTokens int64 `json:"cache_creation_tokens"`
TotalTokens int64 `json:"total_tokens"`
} }
type failDetail struct { type failDetail struct {
@@ -116,6 +116,8 @@ func hasNonZeroTokenUsage(detail usage.Detail) bool {
detail.OutputTokens != 0 || detail.OutputTokens != 0 ||
detail.ReasoningTokens != 0 || detail.ReasoningTokens != 0 ||
detail.CachedTokens != 0 || detail.CachedTokens != 0 ||
detail.CacheReadTokens != 0 ||
detail.CacheCreationTokens != 0 ||
detail.TotalTokens != 0 detail.TotalTokens != 0
} }
@@ -356,17 +358,7 @@ func ParseClaudeUsage(data []byte) usage.Detail {
if !usageNode.Exists() { if !usageNode.Exists() {
return usage.Detail{} return usage.Detail{}
} }
detail := usage.Detail{ return parseClaudeUsageNode(usageNode)
InputTokens: usageNode.Get("input_tokens").Int(),
OutputTokens: usageNode.Get("output_tokens").Int(),
CachedTokens: usageNode.Get("cache_read_input_tokens").Int(),
}
if detail.CachedTokens == 0 {
// fall back to creation tokens when read tokens are absent
detail.CachedTokens = usageNode.Get("cache_creation_input_tokens").Int()
}
detail.TotalTokens = detail.InputTokens + detail.OutputTokens
return detail
} }
func ParseClaudeStreamUsage(line []byte) (usage.Detail, bool) { func ParseClaudeStreamUsage(line []byte) (usage.Detail, bool) {
@@ -378,16 +370,24 @@ func ParseClaudeStreamUsage(line []byte) (usage.Detail, bool) {
if !usageNode.Exists() { if !usageNode.Exists() {
return usage.Detail{}, false return usage.Detail{}, false
} }
return parseClaudeUsageNode(usageNode), true
}
func parseClaudeUsageNode(usageNode gjson.Result) usage.Detail {
cacheReadTokens := usageNode.Get("cache_read_input_tokens").Int()
cacheCreationTokens := usageNode.Get("cache_creation_input_tokens").Int()
detail := usage.Detail{ detail := usage.Detail{
InputTokens: usageNode.Get("input_tokens").Int(), InputTokens: usageNode.Get("input_tokens").Int(),
OutputTokens: usageNode.Get("output_tokens").Int(), OutputTokens: usageNode.Get("output_tokens").Int(),
CachedTokens: usageNode.Get("cache_read_input_tokens").Int(), CachedTokens: cacheReadTokens,
CacheReadTokens: cacheReadTokens,
CacheCreationTokens: cacheCreationTokens,
} }
if detail.CachedTokens == 0 { if detail.CachedTokens == 0 {
detail.CachedTokens = usageNode.Get("cache_creation_input_tokens").Int() detail.CachedTokens = detail.CacheCreationTokens
} }
detail.TotalTokens = detail.InputTokens + detail.OutputTokens detail.TotalTokens = detail.InputTokens + detail.OutputTokens
return detail, true return detail
} }
func parseGeminiFamilyUsageDetail(node gjson.Result) usage.Detail { func parseGeminiFamilyUsageDetail(node gjson.Result) usage.Detail {
+7 -5
View File
@@ -34,11 +34,13 @@ type Failure struct {
// Detail holds the token usage breakdown. // Detail holds the token usage breakdown.
type Detail struct { type Detail struct {
InputTokens int64 InputTokens int64
OutputTokens int64 OutputTokens int64
ReasoningTokens int64 ReasoningTokens int64
CachedTokens int64 CachedTokens int64
TotalTokens int64 CacheReadTokens int64
CacheCreationTokens int64
TotalTokens int64
} }
type requestedModelAliasContextKey struct{} type requestedModelAliasContextKey struct{}