summaryrefslogtreecommitdiff
path: root/internal/usage/observer.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/usage/observer.go')
-rw-r--r--internal/usage/observer.go52
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
}