From 5dcb3cd4fc049135fb78e2f455378dc0bb62a8a4 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 2 Jul 2026 23:07:57 +0000 Subject: [PATCH] Expose cache usage in adapters --- internal/adapter/cli/run.go | 6 ++++- internal/adapter/cli/run_test.go | 32 +++++++++++++++++++++++++++ internal/adapter/http/dto.go | 2 ++ internal/adapter/http/handler.go | 2 ++ internal/adapter/http/handler_test.go | 27 +++++++++++++++++----- 5 files changed, 63 insertions(+), 6 deletions(-) diff --git a/internal/adapter/cli/run.go b/internal/adapter/cli/run.go index e04f130..0099ca5 100644 --- a/internal/adapter/cli/run.go +++ b/internal/adapter/cli/run.go @@ -606,7 +606,7 @@ func printSummary(stderr io.Writer, res *domain.RunResult) { if res == nil { return } - fmt.Fprintf(stderr, "prompt=%s@%s selected_profile=%s model=%s validation=%s mode=%s validation_errors=%d prompt_hash=%s inputs=%d usage=%d/%d/%d\n", + fmt.Fprintf(stderr, "prompt=%s@%s selected_profile=%s model=%s validation=%s mode=%s validation_errors=%d prompt_hash=%s inputs=%d usage=%d/%d/%d", res.PromptID, res.PromptVersion, res.SelectedProfileID, @@ -620,6 +620,10 @@ func printSummary(stderr io.Writer, res *domain.RunResult) { res.Usage.CompletionTokens, res.Usage.TotalTokens, ) + if res.Usage.CachedTokens != 0 || res.Usage.CacheWriteTokens != 0 { + fmt.Fprintf(stderr, " cached_tokens=%d cache_write_tokens=%d", res.Usage.CachedTokens, res.Usage.CacheWriteTokens) + } + fmt.Fprintln(stderr) } func printUsage(w io.Writer) { diff --git a/internal/adapter/cli/run_test.go b/internal/adapter/cli/run_test.go index ef0e61f..7b7896a 100644 --- a/internal/adapter/cli/run_test.go +++ b/internal/adapter/cli/run_test.go @@ -1064,6 +1064,38 @@ func TestWriteOutputAndSummaryUseSeparateWriters(t *testing.T) { if !strings.Contains(stderr.String(), "prompt=p@1") { t.Fatalf("expected summary on stderr, got %q", stderr.String()) } + if strings.Contains(stderr.String(), "cached_tokens=") || strings.Contains(stderr.String(), "cache_write_tokens=") { + t.Fatalf("expected zero cache usage to be omitted from summary, got %q", stderr.String()) + } +} + +func TestPrintSummaryIncludesCacheUsageWhenPresent(t *testing.T) { + var stderr bytes.Buffer + + printSummary(&stderr, &domain.RunResult{ + PromptID: "p", + PromptVersion: "1", + SelectedProfileID: "exec", + ModelName: "m", + Validation: domain.ValidationResult{Status: domain.ValidationPassed, Mode: domain.ValidationBasic}, + RenderedPromptHash: "h", + InputHashes: map[string]string{"in": "x"}, + Usage: domain.TokenUsage{ + PromptTokens: 10, + CompletionTokens: 5, + TotalTokens: 15, + CachedTokens: 0, + CacheWriteTokens: 3, + }, + }) + + summary := stderr.String() + if !strings.Contains(summary, "usage=10/5/15") { + t.Fatalf("expected base usage summary, got %q", summary) + } + if !strings.Contains(summary, "cached_tokens=0 cache_write_tokens=3") { + t.Fatalf("expected cache usage in summary, got %q", summary) + } } type cliTestLibrary struct { diff --git a/internal/adapter/http/dto.go b/internal/adapter/http/dto.go index 0999dcd..2113b48 100644 --- a/internal/adapter/http/dto.go +++ b/internal/adapter/http/dto.go @@ -86,6 +86,8 @@ type tokenUsageDTO struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` + CachedTokens int `json:"cached_tokens"` + CacheWriteTokens int `json:"cache_write_tokens"` } type validationDTO struct { diff --git a/internal/adapter/http/handler.go b/internal/adapter/http/handler.go index 27ee53a..73a4b73 100644 --- a/internal/adapter/http/handler.go +++ b/internal/adapter/http/handler.go @@ -105,6 +105,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { PromptTokens: res.Usage.PromptTokens, CompletionTokens: res.Usage.CompletionTokens, TotalTokens: res.Usage.TotalTokens, + CachedTokens: res.Usage.CachedTokens, + CacheWriteTokens: res.Usage.CacheWriteTokens, }, StartTime: res.StartTime, EndTime: res.EndTime, diff --git a/internal/adapter/http/handler_test.go b/internal/adapter/http/handler_test.go index ec1b130..c05bfa4 100644 --- a/internal/adapter/http/handler_test.go +++ b/internal/adapter/http/handler_test.go @@ -66,11 +66,17 @@ func TestHandlerPostRunsSuccessWithExplicitProfileID(t *testing.T) { APIKeyEnv: envName, }, InputHashes: map[string]string{"transcript": "h1"}, - Usage: domain.TokenUsage{PromptTokens: 1, CompletionTokens: 2, TotalTokens: 3}, - StartTime: start, - EndTime: end, - Duration: 2 * time.Second, - RawOutput: "hello", + Usage: domain.TokenUsage{ + PromptTokens: 1, + CompletionTokens: 2, + TotalTokens: 3, + CachedTokens: 4, + CacheWriteTokens: 5, + }, + StartTime: start, + EndTime: end, + Duration: 2 * time.Second, + RawOutput: "hello", }} h := NewHandler(r) @@ -111,6 +117,13 @@ func TestHandlerPostRunsSuccessWithExplicitProfileID(t *testing.T) { if metadata["model_name"] != "m1" || metadata["endpoint"] != "http://llm/v1" { t.Fatalf("unexpected model metadata: name=%#v endpoint=%#v", metadata["model_name"], metadata["endpoint"]) } + usage := metadata["usage"].(map[string]any) + if usage["prompt_tokens"] != float64(1) || usage["completion_tokens"] != float64(2) || usage["total_tokens"] != float64(3) { + t.Fatalf("unexpected base usage metadata: %#v", usage) + } + if usage["cached_tokens"] != float64(4) || usage["cache_write_tokens"] != float64(5) { + t.Fatalf("unexpected cache usage metadata: %#v", usage) + } modelParams := metadata["model_params"].(map[string]any) if modelParams["api_key_env"] != envName { t.Fatalf("expected model_params.api_key_env=%q, got %#v", envName, modelParams["api_key_env"]) @@ -171,6 +184,10 @@ func TestHandlerPostRunsSuccessUsingPromptDefaultProfile(t *testing.T) { if metadata["selected_profile_id"] != "prompt-default" { t.Fatalf("expected selected_profile_id from result, got %#v", metadata["selected_profile_id"]) } + usage := metadata["usage"].(map[string]any) + if usage["cached_tokens"] != float64(0) || usage["cache_write_tokens"] != float64(0) { + t.Fatalf("expected zero cache usage fields to be included, got %#v", usage) + } } func TestHandlerModelOverrideMapsAllSupportedExecutionFields(t *testing.T) {