Usage reports what a request cost. Its inclusion semantics are normalised
across providers, which is the single most important thing on this page.
Reference#
| Field | Type | Description |
|---|---|---|
| InputTokens | int | Every token of input, **including** any served from or written to a cache. This is what you are billed for. Zero value: not reported |
| OutputTokens | int | Every token the model generated. Reasoning tokens are inside this figure — except on Gemini, where they are excluded entirely and this under-reports. Zero value: not reported |
| CacheReadTokens | int | Tokens served from a prompt cache, usually at a large discount. Part of InputTokens, not additional to it — so this is how much of your bill was discounted. Zero value: not reported |
| CacheWriteTokens | int | Tokens written to a prompt cache, usually at a premium. Part of InputTokens. Only Anthropic reports this; elsewhere it is always zero. Zero value: not reported |
Methods#
Caveats
InputTokensis the total input, cached tokens included. The cache fields are a breakdown of it, never an addition — soTotalTokens()deliberately does not add them again.- Zero means "not reported", not "zero tokens".
CacheWriteTokensis Anthropic-only. The other wire formats have no such field, so it is always zero elsewhere.- Reasoning tokens are not surfaced. They sit inside
OutputTokens— except on Gemini, wherethoughtsTokenCountis excluded from the output count, soOutputTokensgenuinely under-reports what you are billed. - On OpenAI and openaicompat, streaming usage requires the host to honour
stream_options.include_usage; many compatible hosts do not, and report zero silently.
Why this needed normalising#
Deep diveWhat each provider sends before the adapters converge
OpenAI and Gemini report a cache figure that is a subset of the prompt
count — their prompt_tokens already includes it.
Anthropic reports cache figures disjoint from its input count; its
input_tokens excludes them.
So the same cached conversation reported a different billable input depending on
who served it. Adding InputTokens + CacheReadTokens over-reported on two
providers, and InputTokens alone under-reported on the third.
The adapters now converge on one rule: Anthropic's adds the cache figures in,
the others copy the prompt count. If you were computing
InputTokens + CacheReadTokens yourself against an older build, drop the
addition — you are now double-counting.
Usage#
Reporting a call's cost
fmt.Printf("%d in / %d out / %d total\n",
resp.Usage.InputTokens, resp.Usage.OutputTokens, resp.Usage.TotalTokens())fmt.Printf("%d in / %d out / %d total\n",
resp.Usage.InputTokens, resp.Usage.OutputTokens, resp.Usage.TotalTokens())Accumulating across a conversation
var total skyl.Usage
for {
resp, err := client.Complete(ctx, req)
if err != nil {
return err
}
total = total.Add(resp.Usage)
// …
}var total skyl.Usage
for {
resp, err := client.Complete(ctx, req)
if err != nil {
return err
}
total = total.Add(resp.Usage)
// …
}Cache hit rate
func cacheRate(u skyl.Usage) float64 {
if u.InputTokens == 0 {
return 0
}
// A direct ratio, because CacheReadTokens is part of InputTokens.
return float64(u.CacheReadTokens) / float64(u.InputTokens)
}func cacheRate(u skyl.Usage) float64 {
if u.InputTokens == 0 {
return 0
}
// A direct ratio, because CacheReadTokens is part of InputTokens.
return float64(u.CacheReadTokens) / float64(u.InputTokens)
}Accurate Gemini output counts
var raw struct {
UsageMetadata struct {
ThoughtsTokenCount int `json:"thoughtsTokenCount"`
} `json:"usageMetadata"`
}
if err := json.Unmarshal(resp.Raw, &raw); err == nil {
billed := resp.Usage.OutputTokens + raw.UsageMetadata.ThoughtsTokenCount
_ = billed
}var raw struct {
UsageMetadata struct {
ThoughtsTokenCount int `json:"thoughtsTokenCount"`
} `json:"usageMetadata"`
}
if err := json.Unmarshal(resp.Raw, &raw); err == nil {
billed := resp.Usage.OutputTokens + raw.UsageMetadata.ThoughtsTokenCount
_ = billed
}Troubleshooting#
My totals are higher than the provider's invoice
You are probably adding the cache figures to InputTokens. They are already
inside it. TotalTokens() is input plus output, full stop.
CacheWriteTokens is always zero
You are not on Anthropic. No other wire format reports it. Treat the zero as "not reported".
Streaming reports zero tokens
On OpenAI-family hosts, usage on the terminal event requires
stream_options.include_usage to be honoured. Many compatible hosts ignore it.
Gemini output counts look too low
They are. thoughtsTokenCount is excluded from candidatesTokenCount, so
reasoning tokens are billed but not counted in OutputTokens. Read the counter
from Raw.