package llm import ( "encoding/json" "strings" ) // UsageInfo holds token usage extracted from an LLM API response. type UsageInfo struct { TotalTokens int64 `json:"total_tokens"` PromptTokens int64 `json:"prompt_tokens"` CompletionTokens int64 `json:"completion_tokens"` CacheReadTokens int64 `json:"cache_read_tokens,omitempty"` CacheWriteTokens int64 `json:"cache_write_tokens,omitempty"` } var promptTokensPaths = []string{ "usage.prompt_tokens", // OpenAI standard "prompt_tokens", // flat at root "data.usage.prompt_tokens", // wrapped in data layer } var completionTokensPaths = []string{ "usage.completion_tokens", // OpenAI standard "completion_tokens", // flat at root "data.usage.completion_tokens", // wrapped in data layer } var cacheReadTokensPaths = []string{ "usage.cache_read_input_tokens", // Anthropic "cache_read_input_tokens", // flat at root "usage.prompt_tokens_details.cache_tokens_hit", // some providers "usage.prompt_tokens_details.cache_tokens", // some providers } var cacheWriteTokensPaths = []string{ "usage.cache_creation_input_tokens", // Anthropic / proxy "cache_creation_input_tokens", // flat at root } // totalTokensPaths is an ordered list of JSON paths to try when extracting // total token count from a response body. Paths are dot-separated keys that // navigate through nested map[string]any objects. The first match wins. var totalTokensPaths = []string{ "usage.total_tokens", // OpenAI standard "total_tokens", // flat at root "data.usage.total_tokens", // wrapped in data layer (some proxy APIs) } // resolveUsage parses raw JSON bytes into a map and extracts token usage // by probing configured paths sequentially. Returns nil if no total_tokens found. func resolveUsage(raw []byte) *UsageInfo { var rawBody map[string]any if err := json.Unmarshal(raw, &rawBody); err != nil { return nil } total, hasAny := probePath(rawBody, totalTokensPaths) prompt, _ := probePath(rawBody, promptTokensPaths) completion, _ := probePath(rawBody, completionTokensPaths) cacheRead, _ := probePath(rawBody, cacheReadTokensPaths) cacheWrite, _ := probePath(rawBody, cacheWriteTokensPaths) if !hasAny && prompt == 0 && completion == 0 { return nil } ui := &UsageInfo{ TotalTokens: total, PromptTokens: prompt, CompletionTokens: completion, CacheReadTokens: cacheRead, CacheWriteTokens: cacheWrite, } // If TotalTokens wasn't explicitly available but we have prompt+completion, compute it. if total == 0 && (prompt > 0 || completion > 0) { ui.TotalTokens = prompt + completion + cacheRead + cacheWrite } return ui } // probePath walks through each candidate path in order, returning the first // int64 value found along with true. Returns (0, false) if none match. func probePath(root map[string]any, paths []string) (int64, bool) { for _, p := range paths { parts := strings.Split(p, ".") var current any = root for _, part := range parts { obj, ok := current.(map[string]any) if !ok { goto next } current, ok = obj[part] if !ok { goto next } } switch v := current.(type) { case float64: return int64(v), true case int64: return v, true case int: return int64(v), true } next: } return 0, false }