@@ -75,6 +75,81 @@ func TestReadTurnUsageAggregatesMultipleIterations(t *testing.T) {
7575 assert .Equal (t , int64 (60 ), usage .CacheReadInputTokens )
7676}
7777
78+ // fallbackUsage is a two-iteration usage payload modeled on a real fallback
79+ // request (Fable attempt, then Opus fallback): the top-level fields mirror only
80+ // the final iteration.
81+ const fallbackUsage = `{"input_tokens":5607,"output_tokens":698,"cache_creation_input_tokens":100,"cache_read_input_tokens":2000,"cache_creation":{"ephemeral_1h_input_tokens":100,"ephemeral_5m_input_tokens":0},"iterations":[{"type":"message","model":"claude-fable-5","input_tokens":5607,"output_tokens":2,"cache_creation_input_tokens":50,"cache_read_input_tokens":1000},{"type":"fallback_message","model":"claude-opus-4-8","input_tokens":5607,"output_tokens":698,"cache_creation_input_tokens":100,"cache_read_input_tokens":2000}]}`
82+
83+ func TestReadTurnUsageSumsFallbackIterations (t * testing.T ) {
84+ path := filepath .Join (t .TempDir (), "transcript.jsonl" )
85+ writeTranscript (t , path , []string {
86+ `{"type":"user","message":{"role":"user","content":[{"type":"text","text":"hello"}]}}` ,
87+ `{"type":"assistant","requestId":"req_001","message":{"role":"assistant","content":[{"type":"text","text":"hi"}],"usage":` + fallbackUsage + `}}` ,
88+ })
89+
90+ usage , err := ReadTurnUsage (path )
91+ require .NoError (t , err )
92+ require .NotNil (t , usage )
93+
94+ // Both iterations are billed, so all fields are summed across them.
95+ assert .Equal (t , int64 (11214 ), usage .InputTokens )
96+ assert .Equal (t , int64 (700 ), usage .OutputTokens )
97+ assert .Equal (t , int64 (150 ), usage .CacheCreationInputTokens )
98+ assert .Equal (t , int64 (3000 ), usage .CacheReadInputTokens )
99+ }
100+
101+ func TestReadTurnUsageSingleIterationMatchesTopLevel (t * testing.T ) {
102+ path := filepath .Join (t .TempDir (), "transcript.jsonl" )
103+ writeTranscript (t , path , []string {
104+ `{"type":"user","message":{"role":"user","content":[{"type":"text","text":"hello"}]}}` ,
105+ // The single iteration duplicates the top-level fields, as observed in
106+ // real transcripts; behavior must be identical to no iterations at all.
107+ `{"type":"assistant","requestId":"req_001","message":{"role":"assistant","content":[{"type":"text","text":"hi"}],"usage":{"input_tokens":100,"output_tokens":50,"cache_creation_input_tokens":200,"cache_read_input_tokens":300,"iterations":[{"type":"message","input_tokens":100,"output_tokens":50,"cache_creation_input_tokens":200,"cache_read_input_tokens":300}]}}}` ,
108+ })
109+
110+ usage , err := ReadTurnUsage (path )
111+ require .NoError (t , err )
112+ require .NotNil (t , usage )
113+
114+ assert .Equal (t , int64 (100 ), usage .InputTokens )
115+ assert .Equal (t , int64 (50 ), usage .OutputTokens )
116+ assert .Equal (t , int64 (200 ), usage .CacheCreationInputTokens )
117+ assert .Equal (t , int64 (300 ), usage .CacheReadInputTokens )
118+ }
119+
120+ func TestReadTurnUsageDeduplicatesFallbackIterations (t * testing.T ) {
121+ path := filepath .Join (t .TempDir (), "transcript.jsonl" )
122+ writeTranscript (t , path , []string {
123+ `{"type":"user","message":{"role":"user","content":[{"type":"text","text":"hello"}]}}` ,
124+ // Streaming split: both entries repeat the same usage incl. iterations.
125+ `{"type":"assistant","requestId":"req_001","message":{"role":"assistant","content":[{"type":"thinking","thinking":"..."}],"usage":` + fallbackUsage + `}}` ,
126+ `{"type":"assistant","requestId":"req_001","message":{"role":"assistant","content":[{"type":"text","text":"hi"}],"usage":` + fallbackUsage + `}}` ,
127+ })
128+
129+ usage , err := ReadTurnUsage (path )
130+ require .NoError (t , err )
131+ require .NotNil (t , usage )
132+
133+ // Iterations are summed once per requestId, not once per entry.
134+ assert .Equal (t , int64 (11214 ), usage .InputTokens )
135+ assert .Equal (t , int64 (700 ), usage .OutputTokens )
136+ }
137+
138+ func TestReadTurnUsageEmptyIterations (t * testing.T ) {
139+ path := filepath .Join (t .TempDir (), "transcript.jsonl" )
140+ writeTranscript (t , path , []string {
141+ `{"type":"user","message":{"role":"user","content":[{"type":"text","text":"hello"}]}}` ,
142+ `{"type":"assistant","requestId":"req_001","message":{"role":"assistant","content":[{"type":"text","text":"hi"}],"usage":{"input_tokens":100,"output_tokens":50,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"iterations":[]}}}` ,
143+ })
144+
145+ usage , err := ReadTurnUsage (path )
146+ require .NoError (t , err )
147+ require .NotNil (t , usage )
148+
149+ assert .Equal (t , int64 (100 ), usage .InputTokens )
150+ assert .Equal (t , int64 (50 ), usage .OutputTokens )
151+ }
152+
78153func TestReadTurnUsageIgnoresPreviousTurns (t * testing.T ) {
79154 path := filepath .Join (t .TempDir (), "transcript.jsonl" )
80155 writeTranscript (t , path , []string {
0 commit comments