Repository navigation
fix(ai-proxy): include cache tokens in Anthropic prompt usage and read them from message_delta - #14028
fix(ai-proxy): include cache tokens in Anthropic prompt usage and read them from message_delta#14028AlinsRan wants to merge 4 commits into
Conversation
…d them from message_delta Anthropic reports cache_read_input_tokens and cache_creation_input_tokens alongside input_tokens instead of inside it. Usage for the Anthropic Messages protocol now follows LiteLLM's calculate_usage: - prompt_tokens = input_tokens + cache_read_input_tokens + cache_creation_input_tokens, and total_tokens = prompt_tokens + completion_tokens, the same meaning as OpenAI's prompt_tokens. - Streaming usage is accumulated across message_start and message_delta, so cache tokens that a provider reports only in message_delta are no longer dropped. - The Anthropic-to-OpenAI converter deducts cache_creation tokens as well as cached tokens when building Anthropic input_tokens.
…d shapes The converter now resolves cache-creation tokens from the top-level usage.cache_creation_input_tokens and from prompt_tokens_details cache_creation_tokens / cache_write_tokens, in LiteLLM's order, in addition to prompt_tokens_details.cache_creation_input_tokens.
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
3 open findings
The function name/comment says “accumulate”, but the implementation is actually a field-wise… · Newcache_creation_tokenswill raise an error if it’s ever called with a non-tableusage(e.g.,… · New The Chinese docs line mixes singular/plural (“prompt token”) and English “token” wording in a way… · New
What changed in this PR
This PR aligns Anthropic Messages token accounting with OpenAI semantics by including cache read/write tokens in prompt/total usage and ensuring streaming usage captures cache tokens even when reported only in message_delta.
Changes:
- Adjust Anthropic usage calculation so
prompt_tokens = input_tokens + cache_read_input_tokens + cache_creation_input_tokensandtotal_tokens = prompt_tokens + completion_tokens. - Update Anthropic streaming parsing to merge usage fields across
message_startandmessage_delta. - Extend the Anthropic↔OpenAI converter and test fixtures/docs to prevent double-counting cache-creation tokens and reflect the new semantics.
| File | Description |
|---|---|
apisix/plugins/ai-protocols/anthropic-messages.lua |
Computes prompt/total tokens including cache tokens and merges streaming usage fields across events. |
apisix/plugins/ai-protocols/converters/anthropic-messages-to-openai-chat.lua |
Deduces cache-creation tokens from multiple upstream shapes and subtracts them when building Anthropic input_tokens. |
t/plugin/ai-proxy-anthropic.t |
Updates/adds tests for cache token handling in logging and conversion, incl. cache tokens reported in message_delta. |
t/plugin/ai-rate-limiting.t |
Adds coverage for rate limiting with total_tokens including cache tokens for native Anthropic. |
t/fixtures/anthropic/messages-streaming-with-cache-in-delta.sse |
Adds an SSE fixture where cache tokens appear only in message_delta. |
docs/en/latest/plugins/*.md |
Documents that prompt tokens include cache read/write tokens and provides Anthropic formula. |
docs/zh/latest/plugins/*.md |
Same documentation updates in Chinese. |
🧠 Review effort: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
Non-streaming converted responses alter llm_raw_usage despite the PR’s compatibility claim.
4 open findings
Preserve raw usage before converting non-streaming responses · Newcache_creation_tokenswill raise an error if it’s ever called with a non-tableusage(e.g.,… The function name/comment says “accumulate”, but the implementation is actually a field-wise… The Chinese docs line mixes singular/plural (“prompt token”) and English “token” wording in a way…
🧠 Review effort: Balanced
- Rename accumulate_usage to merge_usage_fields and explain that later events overwrite fields because the values are cumulative. - Guard cache_creation_tokens against a non-table usage. - Add expression-strategy coverage for the converted non-streaming path, where ctx.llm_raw_usage holds the converted Anthropic usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
A non-blocking accounting observation for the author to decide whether to address in this PR or in a follow-up: The converter now recognizes For example, an Anthropic client with an OpenAI-compatible upstream can receive this final streaming usage: {
"prompt_tokens": 100,
"completion_tokens": 10,
"cache_creation_input_tokens": 30,
"prompt_tokens_details": { "cached_tokens": 20 }
}A focused probe of the reviewed Lua modules shows that the converted client response reports Relevant code: converter and streaming usage extraction. A possible fix is to share the cache-write field extraction rules between conversion and usage normalization, preserving raw usage, and add streaming E2E assertions for both the response and access-log cache counts for all three field shapes. |
… converter The converter read cache-write tokens from several field shapes, while openai-chat usage extraction (used for native OpenAI responses and for converted streaming responses) only read prompt_tokens_details.cache_creation_input_tokens. On the converted streaming path the client received the right cache_creation_input_tokens but $llm_cache_creation_input_tokens logged 0. Cache reads differed the same way: openai-chat fell back to DeepSeek's prompt_cache_hit_tokens, the converter did not. Move cache_read_tokens() and cache_creation_tokens() into openai-chat and use them from both places. Raw usage is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|


Description
Anthropic Messages requests with prompt caching report wrong token usage. Users see a cache hit ratio above 100% (cache reads / prompt tokens), and token-based rate limits and metrics count fewer tokens than the request actually used.
How the two protocols define input tokens
prompt_tokensinput_tokensprompt_tokens_details.*)prompt_tokens_details.cached_tokenscache_read_input_tokensprompt_tokens_details.cache_write_tokenscache_creation_input_tokensprompt_tokensinput_tokens + cache_read_input_tokens + cache_creation_input_tokensmessage_start, then cumulative values inmessage_deltaAPISIX uses the OpenAI meaning for
$llm_prompt_tokenson every protocol:prompt_tokens= total input, including cache reads and writestotal_tokens=prompt_tokens + completion_tokensBugs fixed
prompt_tokenswasinput_tokensonly, so it did not include cache tokensprompt_tokens = input + cache_read + cache_creationmessage_start. Some providers report them only inmessage_delta, so they were logged as0message_startandmessage_delta, then computecached_tokensfromprompt_tokens, so cache writes were counted twiceinput_tokens = max(0, prompt - cached - cache_write)$llm_cache_creation_input_tokenswas0unless the upstream usedprompt_tokens_details.cache_creation_input_tokensopenai-chat(below)Example
Request with 100,000 input tokens: 90,000 read from cache, 9,000 written to cache, 1,000 uncached.
$llm_prompt_tokensinput_tokenssent to the clientImplementation
anthropic-messages.lua: computeprompt_tokens/total_tokensas above; in streaming, merge raw usage frommessage_startandmessage_deltabefore computing. This matches LiteLLM (AnthropicConfig.calculate_usage, streaming_handle_usage).openai-chat.lua: new shared helpers, used byparse_sse_event,extract_usageand the converter:cache_read_tokens():prompt_tokens_details.cached_tokens, then DeepSeekprompt_cache_hit_tokens.cache_creation_tokens(): top-levelcache_creation_input_tokens, thenprompt_tokens_details.cache_creation_tokens/cache_write_tokens/cache_creation_input_tokens(LiteLLM order).anthropic-messages-to-openai-chat.lua: use the helpers on the non-streaming path and both streaming paths.Corrected values
No configuration, schema or API changes. Only requests that use prompt caching on the paths above report different values.
$llm_prompt_tokens,$llm_total_tokens,llm_summary$llm_cache_read_input_tokens,$llm_cache_creation_input_tokens0in the cases of bugs 2 and 4apisix_llm_prompt_tokens(_dist)ai-rate-limitingwithtotal_tokens/prompt_tokensai-rate-limitingwithexpressioninput_tokensno longer includes cache writes (fixture: 20 → 15)prompt_tokens_details.cache_write_tokensis now recognizedctx.llm_raw_usagestays the raw upstream usage, except on the converted non-streaming path, where it already held the converted Anthropic usage before this PR (moderation plugins use it to build deny responses in the client protocol). This PR keeps that behavior.Tests
t/plugin/ai-proxy-anthropic.tmessage_delta; converted path$llm_prompt_tokensequals upstreamprompt_tokens. TEST 70: converter cache-write field shapes. TEST 71-74: converted streaming logs match client values for each field shape and DeepSeek. TEST 75: unit test ofextract_usage/parse_sse_eventt/plugin/ai-rate-limiting.ttotal_tokenst/plugin/ai-rate-limiting-expression.tinput_tokens + cache_read_input_tokens + cache_creation_input_tokensequals upstreamprompt_tokensAll new and changed tests fail without the fix.
Docs for
ai-proxy,ai-proxy-multi,ai-rate-limitingandprometheus(en/zh) are updated.Which issue(s) this PR fixes:
Fixes #
Checklist
This is a bug fix. It does not change configuration, schemas or APIs.