diff options
Diffstat (limited to 'internal/usage/observer.go')
| -rw-r--r-- | internal/usage/observer.go | 52 |
1 files changed, 44 insertions, 8 deletions
diff --git a/internal/usage/observer.go b/internal/usage/observer.go index 808781f..71c1379 100644 --- a/internal/usage/observer.go +++ b/internal/usage/observer.go @@ -95,18 +95,28 @@ func (o *Observer) parseSSELine(line []byte) { o.parseJSON(payload) } +type tokenDetails struct { + CachedTokens *int64 `json:"cached_tokens"` + CacheWriteTokens *int64 `json:"cache_write_tokens"` +} + type usageFields struct { - PromptTokens *int64 `json:"prompt_tokens"` - CompletionTokens *int64 `json:"completion_tokens"` - TotalTokens *int64 `json:"total_tokens"` - InputTokens *int64 `json:"input_tokens"` - OutputTokens *int64 `json:"output_tokens"` - CacheCreationInputTokens *int64 `json:"cache_creation_input_tokens"` - CacheReadInputTokens *int64 `json:"cache_read_input_tokens"` + PromptTokens *int64 `json:"prompt_tokens"` + CompletionTokens *int64 `json:"completion_tokens"` + TotalTokens *int64 `json:"total_tokens"` + InputTokens *int64 `json:"input_tokens"` + OutputTokens *int64 `json:"output_tokens"` + CacheCreationInputTokens *int64 `json:"cache_creation_input_tokens"` + CacheReadInputTokens *int64 `json:"cache_read_input_tokens"` + PromptTokensDetails *tokenDetails `json:"prompt_tokens_details"` + InputTokensDetails *tokenDetails `json:"input_tokens_details"` } type responseEnvelope struct { - Usage *usageFields `json:"usage"` + Usage *usageFields `json:"usage"` + Response *struct { + Usage *usageFields `json:"usage"` + } `json:"response"` Message *struct { Usage *usageFields `json:"usage"` } `json:"message"` @@ -131,16 +141,22 @@ func (o *Observer) parseJSON(payload []byte) { if envelope.Message != nil && envelope.Message.Usage != nil { o.apply(envelope.Message.Usage) } + if envelope.Response != nil && envelope.Response.Usage != nil { + o.apply(envelope.Response.Usage) + } } func (o *Observer) apply(fields *usageFields) { + inputReported := false if fields.PromptTokens != nil { o.usage.InputTokens = *fields.PromptTokens o.found = true + inputReported = true } if fields.InputTokens != nil { o.usage.InputTokens = *fields.InputTokens o.found = true + inputReported = true } if fields.CompletionTokens != nil { o.usage.OutputTokens = *fields.CompletionTokens @@ -163,6 +179,26 @@ func (o *Observer) apply(fields *usageFields) { o.usage.CacheReadInputTokens = *fields.CacheReadInputTokens o.found = true } + details := fields.InputTokensDetails + if details == nil { + details = fields.PromptTokensDetails + } + if details != nil { + if details.CachedTokens != nil { + o.usage.CacheReadInputTokens = *details.CachedTokens + o.found = true + } + if details.CacheWriteTokens != nil { + o.usage.CacheCreationInputTokens = *details.CacheWriteTokens + o.found = true + } + // OpenAI reports cached token details as subsets of prompt/input_tokens. + // Normalize them into mutually exclusive buckets before billing. Anthropic + // reports its top-level cache fields separately, so they are not adjusted. + if inputReported && (o.protocol == domain.ProtocolOpenAI || o.protocol == domain.ProtocolOpenAIResponses) { + o.usage.InputTokens = max(0, o.usage.InputTokens-o.usage.CacheReadInputTokens-o.usage.CacheCreationInputTokens) + } + } if !o.explicitTotal && o.found { o.usage.TotalTokens = o.usage.InputTokens + o.usage.OutputTokens } |
