Skip to content
skyl

Usage

Token consumption, with cached tokens as a breakdown of input.

Usage reports what a request cost. Its inclusion semantics are normalised across providers, which is the single most important thing on this page.

Reference#

type Usage struct { InputTokens int OutputTokens int CacheReadTokens int CacheWriteTokens int }
FieldTypeDescription
InputTokensintEvery token of input, **including** any served from or written to a cache. This is what you are billed for.
Zero value: not reported
OutputTokensintEvery 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
CacheReadTokensintTokens 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
CacheWriteTokensintTokens 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#

func (u Usage) TotalTokens() int // InputTokens + OutputTokens func (u Usage) Add(other Usage) Usage

Caveats

  • InputTokens is the total input, cached tokens included. The cache fields are a breakdown of it, never an addition — so TotalTokens() deliberately does not add them again.
  • Zero means "not reported", not "zero tokens".
  • CacheWriteTokens is 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, where thoughtsTokenCount is excluded from the output count, so OutputTokens genuinely 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

goCompiles
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

goCompiles
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

goCompiles
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

goCompiles
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.

Edit this page on GitHub