detached bcb8569

fix(provider): treat explicit zero Anthropic cache deltas as measured

commit
bcb85699597e02e9f175c98f2851aed15134cab2
author
brice · · 2026-10-20 04:35 UTC
parent
d2ceb48
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
checks
0/3
2 files · +27 -3
M packages/provider/anthropic.go +2 -2
@@ -854,11 +854,11 @@ func (c *anthropicClient) runStream(ctx context.Context, resp *http.Response, re
854854 if m.Usage.OutputTokens > 0 {
855855 usage.OutputTokens = m.Usage.OutputTokens
856856 }
857- if r := m.Usage.CacheReadInputTokens; r != nil && *r > 0 {
857+ if r := m.Usage.CacheReadInputTokens; r != nil {
858858 usage.CacheReadTokens = *r
859859 cacheReported = true
860860 }
861- if w := m.Usage.CacheCreationInputTokens; w != nil && *w > 0 {
861+ if w := m.Usage.CacheCreationInputTokens; w != nil {
862862 usage.CacheWriteTokens = *w
863863 cacheReported = true
864864 }
@@ -854,11 +854,11 @@ func (c *anthropicClient) runStream(ctx context.Context, resp *http.Response, re
854 if m.Usage.OutputTokens > 0 {854 if m.Usage.OutputTokens > 0 {
855 usage.OutputTokens = m.Usage.OutputTokens855 usage.OutputTokens = m.Usage.OutputTokens
856 }856 }
857- if r := m.Usage.CacheReadInputTokens; r != nil && *r > 0 {857+ if r := m.Usage.CacheReadInputTokens; r != nil {
858 usage.CacheReadTokens = *r858 usage.CacheReadTokens = *r
859 cacheReported = true859 cacheReported = true
860 }860 }
861- if w := m.Usage.CacheCreationInputTokens; w != nil && *w > 0 {861+ if w := m.Usage.CacheCreationInputTokens; w != nil {
862 usage.CacheWriteTokens = *w862 usage.CacheWriteTokens = *w
863 cacheReported = true863 cacheReported = true
864 }864 }
M packages/provider/provider_test.go +25 -1
@@ -555,12 +555,17 @@ func TestAnthropicStreamHappyPath(t *testing.T) {
555555 }
556556
557557 func anthropicStreamUsage(t *testing.T, messageStart string) Usage {
558+ t.Helper()
559+ return anthropicStreamUsageWithDelta(t, messageStart, `{"delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5}}`)
560+}
561+
562+func anthropicStreamUsageWithDelta(t *testing.T, messageStart, messageDelta string) Usage {
558563 t.Helper()
559564 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
560565 w.Header().Set("content-type", "text/event-stream")
561566 w.WriteHeader(http.StatusOK)
562567 _, _ = w.Write([]byte("event: message_start\ndata: " + messageStart + "\n\n" +
563- "event: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}\n\n" +
568+ "event: message_delta\ndata: " + messageDelta + "\n\n" +
564569 "event: message_stop\ndata: {}\n\n"))
565570 }))
566571 defer srv.Close()
@@ -596,6 +601,25 @@ func TestAnthropicStreamKnownCacheMissReportsZeroRatio(t *testing.T) {
596601 }
597602 }
598603
604+func TestAnthropicStreamCacheFieldsInDeltaOnly(t *testing.T) {
605+ start := `{"message":{"usage":{"input_tokens":100,"output_tokens":0}}}`
606+ for _, tc := range []struct {
607+ name string
608+ delta string
609+ want float64
610+ }{
611+ {"known miss", `{"delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5,"cache_read_input_tokens":0,"cache_creation_input_tokens":0}}`, 0},
612+ {"hit", `{"delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5,"cache_read_input_tokens":300,"cache_creation_input_tokens":0}}`, 0.75},
613+ } {
614+ t.Run(tc.name, func(t *testing.T) {
615+ usage := anthropicStreamUsageWithDelta(t, start, tc.delta)
616+ if ratio, ok := usage.CacheHitRatio(); !ok || math.Abs(ratio-tc.want) > 1e-9 {
617+ t.Fatalf("cache hit ratio = %v, %v, want %v, true", ratio, ok, tc.want)
618+ }
619+ })
620+ }
621+}
622+
599623 func TestAnthropicStreamWithoutCacheFieldsLeavesRatioUnknown(t *testing.T) {
600624 usage := anthropicStreamUsage(t, `{"message":{"usage":{"input_tokens":100,"output_tokens":0}}}`)
601625 if ratio, ok := usage.CacheHitRatio(); ok {
@@ -555,12 +555,17 @@ func TestAnthropicStreamHappyPath(t *testing.T) {
555 }555 }
556 556
557 func anthropicStreamUsage(t *testing.T, messageStart string) Usage {557 func anthropicStreamUsage(t *testing.T, messageStart string) Usage {
558+ t.Helper()
559+ return anthropicStreamUsageWithDelta(t, messageStart, `{"delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5}}`)
560+}
561+
562+func anthropicStreamUsageWithDelta(t *testing.T, messageStart, messageDelta string) Usage {
558 t.Helper()563 t.Helper()
559 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {564 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
560 w.Header().Set("content-type", "text/event-stream")565 w.Header().Set("content-type", "text/event-stream")
561 w.WriteHeader(http.StatusOK)566 w.WriteHeader(http.StatusOK)
562 _, _ = w.Write([]byte("event: message_start\ndata: " + messageStart + "\n\n" +567 _, _ = w.Write([]byte("event: message_start\ndata: " + messageStart + "\n\n" +
563- "event: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}\n\n" +568+ "event: message_delta\ndata: " + messageDelta + "\n\n" +
564 "event: message_stop\ndata: {}\n\n"))569 "event: message_stop\ndata: {}\n\n"))
565 }))570 }))
566 defer srv.Close()571 defer srv.Close()
@@ -596,6 +601,25 @@ func TestAnthropicStreamKnownCacheMissReportsZeroRatio(t *testing.T) {
596 }601 }
597 }602 }
598 603
604+func TestAnthropicStreamCacheFieldsInDeltaOnly(t *testing.T) {
605+ start := `{"message":{"usage":{"input_tokens":100,"output_tokens":0}}}`
606+ for _, tc := range []struct {
607+ name string
608+ delta string
609+ want float64
610+ }{
611+ {"known miss", `{"delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5,"cache_read_input_tokens":0,"cache_creation_input_tokens":0}}`, 0},
612+ {"hit", `{"delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5,"cache_read_input_tokens":300,"cache_creation_input_tokens":0}}`, 0.75},
613+ } {
614+ t.Run(tc.name, func(t *testing.T) {
615+ usage := anthropicStreamUsageWithDelta(t, start, tc.delta)
616+ if ratio, ok := usage.CacheHitRatio(); !ok || math.Abs(ratio-tc.want) > 1e-9 {
617+ t.Fatalf("cache hit ratio = %v, %v, want %v, true", ratio, ok, tc.want)
618+ }
619+ })
620+ }
621+}
622+
599 func TestAnthropicStreamWithoutCacheFieldsLeavesRatioUnknown(t *testing.T) {623 func TestAnthropicStreamWithoutCacheFieldsLeavesRatioUnknown(t *testing.T) {
600 usage := anthropicStreamUsage(t, `{"message":{"usage":{"input_tokens":100,"output_tokens":0}}}`)624 usage := anthropicStreamUsage(t, `{"message":{"usage":{"input_tokens":100,"output_tokens":0}}}`)
601 if ratio, ok := usage.CacheHitRatio(); ok {625 if ratio, ok := usage.CacheHitRatio(); ok {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close