From 65e760aa1a0ffef2b7a9e5a92115885acf97769b Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Thu, 28 May 2026 21:34:54 +0800 Subject: [PATCH] feat(usage): include cache tokens in total token calculation and add tests - Updated `TotalTokens` calculation to account for `CacheReadTokens` and `CacheCreationTokens`. - Added tests to validate accurate token aggregation and fallback behavior for `CachedTokens`. --- .../runtime/executor/helps/usage_helpers.go | 2 +- .../executor/helps/usage_helpers_test.go | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/internal/runtime/executor/helps/usage_helpers.go b/internal/runtime/executor/helps/usage_helpers.go index 1c4f4cdf7..295b797d7 100644 --- a/internal/runtime/executor/helps/usage_helpers.go +++ b/internal/runtime/executor/helps/usage_helpers.go @@ -527,7 +527,7 @@ func parseClaudeUsageNode(usageNode gjson.Result) usage.Detail { if detail.CachedTokens == 0 { detail.CachedTokens = detail.CacheCreationTokens } - detail.TotalTokens = detail.InputTokens + detail.OutputTokens + detail.TotalTokens = detail.InputTokens + detail.OutputTokens + detail.CacheReadTokens + detail.CacheCreationTokens return detail } diff --git a/internal/runtime/executor/helps/usage_helpers_test.go b/internal/runtime/executor/helps/usage_helpers_test.go index 58b175f3b..b14a389a0 100644 --- a/internal/runtime/executor/helps/usage_helpers_test.go +++ b/internal/runtime/executor/helps/usage_helpers_test.go @@ -89,6 +89,40 @@ func TestParseOpenAIStreamUsageResponsesFields(t *testing.T) { } } +func TestParseClaudeUsageIncludesCacheTokensInTotal(t *testing.T) { + data := []byte(`{"usage":{"input_tokens":3085,"output_tokens":253,"cache_read_input_tokens":7,"cache_creation_input_tokens":19514}}`) + detail := ParseClaudeUsage(data) + if detail.InputTokens != 3085 { + t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 3085) + } + if detail.OutputTokens != 253 { + t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 253) + } + if detail.CacheReadTokens != 7 { + t.Fatalf("cache read tokens = %d, want %d", detail.CacheReadTokens, 7) + } + if detail.CacheCreationTokens != 19514 { + t.Fatalf("cache creation tokens = %d, want %d", detail.CacheCreationTokens, 19514) + } + if detail.CachedTokens != 7 { + t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 7) + } + if detail.TotalTokens != 22859 { + t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 22859) + } +} + +func TestParseClaudeUsageFallsBackCachedTokensToCacheCreation(t *testing.T) { + data := []byte(`{"usage":{"input_tokens":3085,"output_tokens":253,"cache_creation_input_tokens":19514}}`) + detail := ParseClaudeUsage(data) + if detail.CachedTokens != 19514 { + t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 19514) + } + if detail.TotalTokens != 22852 { + t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 22852) + } +} + func TestParseGeminiCLIUsage_TopLevelUsageMetadata(t *testing.T) { data := []byte(`{"usageMetadata":{"promptTokenCount":11,"candidatesTokenCount":7,"thoughtsTokenCount":3,"totalTokenCount":21,"cachedContentTokenCount":5}}`) detail := ParseGeminiCLIUsage(data)