feat(usage): surface prompt-cache tokens in usage maps - #1
Open
a-florian-h wants to merge 1 commit into
Open
Conversation
OpenAI-compatible providers report prompt-cache hits in
usage.prompt_tokens_details.cached_tokens (OpenAI, z.ai, Kimi, DeepSeek);
Anthropic reports cache_read_input_tokens / cache_creation_input_tokens.
All were dropped — callers cannot measure cache hit rates or true input
cost, and cost estimators re-bill cached tokens at full price.
- openai.ex: usage_from_openai/1 adds :cached_tokens when
prompt_tokens_details.cached_tokens is present (covers both the
non-streaming response and the streamed {:usage, _} event, which shares
this parser). Non-reporting providers are unaffected (key omitted).
- anthropic.ex: usage map adds :cached_tokens (cache reads) and
:cache_creation_input_tokens (cache writes).
- response.ex: document the extended usage shape.
Measured motivation (gc_daemon#3): a 5-turn agent conversation on z.ai
shows 8,384/8,437 cached on steady-state turns — ~99% hit rate invisible
without this field, and billed at 100% by naive estimators.
There was a problem hiding this comment.
Pull request overview
Surfaces provider-reported prompt-cache token counts in the standardized usage maps so downstream callers can measure cache hit rates and compute discounted cached-input costs correctly.
Changes:
- OpenAI: parse and expose
usage.prompt_tokens_details.cached_tokensas:cached_tokensin usage maps (including streamed final{:usage, ...}events). - Anthropic: expose prompt-caching usage as
:cached_tokens(cache_read_input_tokens) and:cache_creation_input_tokens(cache_creation_input_tokens). - Documentation + tests: document the extended
usageshape and add streaming-usage tests for cached vs non-cached usage payloads.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/llm_core/llm/openai_test.exs | Adds streaming tests verifying :cached_tokens is surfaced (or omitted) in final usage events. |
| lib/llm_core/llm/response.ex | Updates Response moduledoc to document the extended usage keys for caching. |
| lib/llm_core/llm/openai.ex | Adds maybe_put_cached_tokens/2 and wires it into OpenAI usage parsing. |
| lib/llm_core/llm/anthropic.ex | Extends Anthropic usage map to include cache read/write token counts. |
Suppressed comments (1)
lib/llm_core/llm/openai.ex:261
usage_from_openai/1only attaches:cached_tokensfor the (prompt_tokens + completion_tokens) usage shape. If an OpenAI-compatible provider returns a usage map containingprompt_tokens_details.cached_tokensalongside onlytotal_tokens(the fallback clause),:cached_tokenswill be silently dropped, which undermines the stated goal of surfacing cache hits whenever they’re reported.
|> maybe_put_cached_tokens(usage)
end
defp usage_from_openai(%{"total_tokens" => total}) do
%{total_tokens: total}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the visibility half of fosferon/gc_daemon#3 (finding F2).
Problem
OpenAI-compatible providers report prompt-cache hits in
usage.prompt_tokens_details.cached_tokens(OpenAI, z.ai, Kimi, DeepSeek); Anthropic reportscache_read_input_tokens/cache_creation_input_tokens.usage_from_openai/1and the Anthropic usage map dropped all of them — so callers can neither measure cache hit rates nor compute true input cost, and cost estimators re-bill cached tokens at full price.As of Aug 2026 every major provider discounts cached input ~90% (z.ai GLM-5.x ~81%: $0.26 vs $1.40/1M), and agent workloads are the best-case consumer (a large stable tool-manifest + system prefix resent every turn). Measured on z.ai via a temporary log line: a 5-turn agent conversation shows
8,384/8,437cached on steady-state turns — a ~99% hit rate that was completely invisible.Change
openai.ex:usage_from_openai/1adds:cached_tokenswhenprompt_tokens_details.cached_tokensis present. Covers both the non-streaming response and the streamed{:usage, _}event (extract_stream_event/1shares the parser). Providers that don't report it are unaffected — the key is simply omitted.anthropic.ex: usage map adds:cached_tokens(fromcache_read_input_tokens) and:cache_creation_input_tokens(writes, billed 1.25x).response.ex: document the extended usage shape.prompt_tokens_details(values from the real z.ai capture above).No behavior change beyond the extra usage keys.
Verification
mix test: 345 tests + 2 properties, 0 failures (includes the 2 new cases).Related
~> 0.4.0predates streaming-usage support entirely (added 0.4.1+); upgrading to 0.6.x getsinclude_usagestreaming + this change together.