@@ -177,7 +177,7 @@ func (r *UsageReporter) buildAdditionalModelRecord(model string, detail usage.De
177177 if model == "" {
178178 return usage.Record {}, false
179179 }
180- detail = normalizeUsageDetailTotal (detail )
180+ detail = normalizeUsageDetailTotal (detail , r . provider , r . executorType )
181181 if ! hasNonZeroTokenUsage (detail ) {
182182 return usage.Record {}, false
183183 }
@@ -201,20 +201,14 @@ func (r *UsageReporter) publishWithOutcome(ctx context.Context, detail usage.Det
201201 if r == nil {
202202 return
203203 }
204- detail = normalizeUsageDetailTotal (detail )
204+ detail = normalizeUsageDetailTotal (detail , r . provider , r . executorType )
205205 r .once .Do (func () {
206206 r .publishRecord (ctx , r .buildRecord (detail , failed , fail ))
207207 })
208208}
209209
210- func normalizeUsageDetailTotal (detail usage.Detail ) usage.Detail {
211- if detail .TotalTokens == 0 {
212- total := detail .InputTokens + detail .OutputTokens + detail .ReasoningTokens
213- if total > 0 {
214- detail .TotalTokens = total
215- }
216- }
217- return detail
210+ func normalizeUsageDetailTotal (detail usage.Detail , provider , executorType string ) usage.Detail {
211+ return usage .EnsureTokenBreakdownForProvider (detail , provider , executorType )
218212}
219213
220214func hasNonZeroTokenUsage (detail usage.Detail ) bool {
@@ -224,7 +218,8 @@ func hasNonZeroTokenUsage(detail usage.Detail) bool {
224218 detail .CachedTokens != 0 ||
225219 detail .CacheReadTokens != 0 ||
226220 detail .CacheCreationTokens != 0 ||
227- detail .TotalTokens != 0
221+ detail .TotalTokens != 0 ||
222+ detail .TokenBreakdown .TotalTokens != 0
228223}
229224
230225// ensurePublished guarantees that a usage record is emitted exactly once.
@@ -556,11 +551,14 @@ func hasOpenAIStyleUsageTokenFields(usageNode gjson.Result) bool {
556551 if ! usageNode .Exists () || ! usageNode .IsObject () {
557552 return false
558553 }
554+ return usageNode .Get ("total_tokens" ).Exists () || hasOpenAIStyleUsageBucketFields (usageNode )
555+ }
556+
557+ func hasOpenAIStyleUsageBucketFields (usageNode gjson.Result ) bool {
559558 return usageNode .Get ("prompt_tokens" ).Exists () ||
560559 usageNode .Get ("input_tokens" ).Exists () ||
561560 usageNode .Get ("completion_tokens" ).Exists () ||
562561 usageNode .Get ("output_tokens" ).Exists () ||
563- usageNode .Get ("total_tokens" ).Exists () ||
564562 usageNode .Get ("prompt_tokens_details.cached_tokens" ).Exists () ||
565563 usageNode .Get ("input_tokens_details.cached_tokens" ).Exists () ||
566564 usageNode .Get ("prompt_tokens_details.cache_write_tokens" ).Exists () ||
@@ -610,6 +608,21 @@ func parseOpenAIStyleUsageNode(usageNode gjson.Result) usage.Detail {
610608 if reasoning .Exists () {
611609 detail .ReasoningTokens = reasoning .Int ()
612610 }
611+ if hasOpenAIStyleUsageBucketFields (usageNode ) {
612+ detail .TokenBreakdown = usage .NewSubsetTokenBreakdown (
613+ detail .InputTokens ,
614+ detail .CacheReadTokens ,
615+ detail .CacheCreationTokens ,
616+ detail .OutputTokens ,
617+ detail .ReasoningTokens ,
618+ detail .TotalTokens ,
619+ )
620+ } else {
621+ detail .TokenBreakdown = usage .NewUnclassifiedTokenBreakdown (detail .TotalTokens )
622+ }
623+ if detail .TotalTokens == 0 {
624+ detail .TotalTokens = detail .TokenBreakdown .TotalTokens
625+ }
613626 return detail
614627}
615628
@@ -665,6 +678,14 @@ func parseClaudeUsageNode(usageNode gjson.Result) usage.Detail {
665678 detail .CachedTokens = detail .CacheCreationTokens
666679 }
667680 detail .TotalTokens = detail .InputTokens + detail .OutputTokens + detail .CacheReadTokens + detail .CacheCreationTokens
681+ detail .TokenBreakdown = usage .NewIndependentTokenBreakdown (
682+ detail .InputTokens ,
683+ detail .CacheReadTokens ,
684+ detail .CacheCreationTokens ,
685+ detail .OutputTokens ,
686+ detail .ReasoningTokens ,
687+ detail .TotalTokens ,
688+ )
668689 return detail
669690}
670691
@@ -681,6 +702,14 @@ func parseGeminiFamilyUsageDetail(node gjson.Result) usage.Detail {
681702 if detail .TotalTokens == 0 {
682703 detail .TotalTokens = detail .InputTokens + detail .OutputTokens + detail .ReasoningTokens
683704 }
705+ detail .TokenBreakdown = usage .NewSeparateReasoningTokenBreakdown (
706+ detail .InputTokens ,
707+ detail .CacheReadTokens ,
708+ detail .CacheCreationTokens ,
709+ detail .OutputTokens ,
710+ detail .ReasoningTokens ,
711+ detail .TotalTokens ,
712+ )
684713 return detail
685714}
686715
@@ -699,11 +728,16 @@ func parseInteractionsUsageDetail(node gjson.Result) usage.Detail {
699728 detail .CacheReadTokens = detail .CachedTokens
700729 }
701730 if detail .TotalTokens == 0 {
702- detail .TotalTokens = detail .InputTokens + detail .OutputTokens + detail .ReasoningTokens + detail .CacheCreationTokens
703- if cacheRead .Exists () {
704- detail .TotalTokens += detail .CacheReadTokens
705- }
731+ detail .TotalTokens = detail .InputTokens + detail .OutputTokens + detail .ReasoningTokens
706732 }
733+ detail .TokenBreakdown = usage .NewSeparateReasoningTokenBreakdown (
734+ detail .InputTokens ,
735+ detail .CacheReadTokens ,
736+ detail .CacheCreationTokens ,
737+ detail .OutputTokens ,
738+ detail .ReasoningTokens ,
739+ detail .TotalTokens ,
740+ )
707741 return detail
708742}
709743
0 commit comments