Skip to content

fix(responses_adapter): read cache_read_tokens from input_tokens_details on Responses usage (#28354) - #28380

Closed
Anai-Guo wants to merge 2 commits into
BerriAI:litellm_internal_stagingfrom
Anai-Guo:fix/responses-cache-read-tokens
Closed

fix(responses_adapter): read cache_read_tokens from input_tokens_details on Responses usage (#28354)#28380
Anai-Guo wants to merge 2 commits into
BerriAI:litellm_internal_stagingfrom
Anai-Guo:fix/responses-cache-read-tokens

Conversation

@Anai-Guo

Copy link
Copy Markdown
Contributor

Summary

Fixes #28354.

AnthropicResponsesStreamWrapper._process_event reads usage.cache_read_input_tokens — an Anthropic-only field name — from the OpenAI Responses usage object on response.completed, so the value is always 0 and clients (Claude Code / Agent SDK, Langfuse, Sentry, billing dashboards) routed through /v1/messages → Responses see cache_read_input_tokens = 0 even when OpenAI is correctly reporting thousands of cached prompt tokens.

The OpenAI Responses API exposes cached tokens at usage.input_tokens_details.cached_tokens — the same pattern the parallel Chat Completions adapter already handles via prompt_tokens_details.cached_tokens (adapters/streaming_iterator.py:280-291).

Change

In responses_adapters/streaming_iterator.py::AnthropicResponsesStreamWrapper._process_event:

  • Read cache_read_tokens from usage.input_tokens_details.cached_tokens (object or dict shape).
  • Fall back to usage.cache_read_input_tokens if the upstream provider already speaks Anthropic usage.
  • Subtract cache_read_tokens from input_tokens to preserve Anthropic's "uncached prompt" semantics, matching the Chat Completions path.
  • Drop the dead first assignment that wrote input_tokens_details / output_tokens_details into the cache-token variables before immediately overwriting them.

cache_creation_input_tokens keeps reading the Anthropic-named field only — OpenAI Responses has no direct equivalent, so emitting 0 there is correct.

Test plan

AI-assisted, human reviewed.

…ils on Responses usage

The Anthropic→Responses streaming adapter was reading
usage.cache_read_input_tokens (an Anthropic-only field) and so always
emitted cache_read_input_tokens=0 to Anthropic-format clients, even
when OpenAI's underlying response correctly reported a non-zero
cached_tokens. This broke observability for Anthropic-compatible
clients (Claude Code, Claude Agent SDK, dashboards, Sentry, Langfuse,
billing) routing OpenAI Responses traffic through LiteLLM.

Read cached prompt tokens from usage.input_tokens_details.cached_tokens
(parallel to the existing Chat Completions path's use of
prompt_tokens_details.cached_tokens), fall back to the Anthropic-named
field if the upstream provider already speaks Anthropic usage, and
subtract cached from input_tokens to preserve Anthropic's 'uncached'
semantics for the input_tokens count.

Also drops the dead first assignment that stored
input_tokens_details / output_tokens_details into the cache token
variables before immediately overwriting them.

Fixes BerriAI#28354
@codspeed-hq

codspeed-hq Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing Anai-Guo:fix/responses-cache-read-tokens (b31aceb) with main (79b4578)

Open in CodSpeed

@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...s_through/responses_adapters/streaming_iterator.py 0.00% 11 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where AnthropicResponsesStreamWrapper._process_event was reading cache_read_input_tokens (an Anthropic-only field) from OpenAI Responses usage objects, causing cache_read_input_tokens to always be 0 on the OpenAI Responses path. The fix reads cached_tokens from usage.input_tokens_details (mirroring the Chat Completions adapter) and subtracts it from input_tokens to match Anthropic's uncached-count semantics.

  • Reads cache_read_tokens from usage.input_tokens_details.cached_tokens for OpenAI Responses, falling back to usage.cache_read_input_tokens for Anthropic-native upstreams.
  • Subtracts cache_read_tokens from input_tokens unconditionally across both paths — an issue flagged in a prior review thread since Anthropic's input_tokens is already the uncached count, causing double-subtraction on the fallback path.
  • Drops the two dead first assignments that stored input_tokens_details/output_tokens_details objects into the cache-token variables before immediately overwriting them.

Confidence Score: 4/5

Safe to merge only after the subtraction-on-fallback bug is resolved; the OpenAI Responses path fix itself is correct.

The OpenAI Responses path fix (reading input_tokens_details.cached_tokens) is correct and mirrors the Chat Completions adapter. However, the unconditional subtraction of cache_read_tokens from input_tokens is applied on both the OpenAI and Anthropic fallback paths. On Anthropic-native upstreams, input_tokens already carries the uncached count, so the subtraction produces a value that is too low by the number of cached tokens — an active misreporting bug on the fallback code path, flagged in a prior review thread that remains unresolved.

litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py — specifically the subtraction guard and the fallback branch around lines 273–286.

Important Files Changed

Filename Overview
litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py Reads cache_read_tokens from usage.input_tokens_details.cached_tokens (OpenAI Responses path) with fallback to cache_read_input_tokens (Anthropic path), then subtracts cached tokens from input_tokens; the subtraction is applied unconditionally to both paths, which double-subtracts on Anthropic-native upstreams where input_tokens is already the uncached count (flagged in a prior review thread).

Reviews (2): Last reviewed commit: "fix(responses_adapter): read cache_read_..." | Re-trigger Greptile

Comment on lines +273 to +286
# Fall back to Anthropic-style direct fields if the upstream
# provider already speaks Anthropic usage.
if not cache_read_tokens:
cache_read_tokens = int(
getattr(usage, "cache_read_input_tokens", 0) or 0
)
cache_creation_tokens = int(
getattr(usage, "cache_creation_input_tokens", 0) or 0
)
cache_read_tokens = int(
getattr(usage, "cache_read_input_tokens", 0) or 0
)
# Anthropic semantics: ``input_tokens`` is the *uncached* prompt
# count. OpenAI reports total prompt tokens (cached + uncached),
# so subtract to keep client-side accounting consistent.
if cache_read_tokens and input_tokens >= cache_read_tokens:
input_tokens = input_tokens - cache_read_tokens

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Fallback + subtraction produces wrong input_tokens for Anthropic-native upstreams

When input_tokens_details is absent (Anthropic providers don't include it) and cache_read_input_tokens > 0, the fallback correctly captures cached tokens — but the unconditional subtraction that follows then reduces an input_tokens value that is already the uncached count (Anthropic semantics). For a request with 3 000 uncached tokens and 500 cached, the emitted input_tokens becomes 2 500 instead of 3 000.

The subtraction is only valid for the OpenAI Responses path (where input_tokens is the total prompt count). It should be skipped whenever the value came from the cache_read_input_tokens fallback.

Comment on lines 259 to +286
if usage is not None:
input_tokens = getattr(usage, "input_tokens", 0) or 0
output_tokens = getattr(usage, "output_tokens", 0) or 0
cache_creation_tokens = getattr(usage, "input_tokens_details", None) # type: ignore[assignment]
cache_read_tokens = getattr(usage, "output_tokens_details", None) # type: ignore[assignment]
# Prefer direct cache fields if present
input_tokens = int(getattr(usage, "input_tokens", 0) or 0)
output_tokens = int(getattr(usage, "output_tokens", 0) or 0)
# OpenAI Responses API exposes cached prompt tokens at
# ``usage.input_tokens_details.cached_tokens`` (parallel to
# ``usage.prompt_tokens_details.cached_tokens`` on Chat Completions).
itd = getattr(usage, "input_tokens_details", None)
if itd is not None:
if isinstance(itd, dict):
cache_read_tokens = int(itd.get("cached_tokens", 0) or 0)
else:
cache_read_tokens = int(
getattr(itd, "cached_tokens", 0) or 0
)
# Fall back to Anthropic-style direct fields if the upstream
# provider already speaks Anthropic usage.
if not cache_read_tokens:
cache_read_tokens = int(
getattr(usage, "cache_read_input_tokens", 0) or 0
)
cache_creation_tokens = int(
getattr(usage, "cache_creation_input_tokens", 0) or 0
)
cache_read_tokens = int(
getattr(usage, "cache_read_input_tokens", 0) or 0
)
# Anthropic semantics: ``input_tokens`` is the *uncached* prompt
# count. OpenAI reports total prompt tokens (cached + uncached),
# so subtract to keep client-side accounting consistent.
if cache_read_tokens and input_tokens >= cache_read_tokens:
input_tokens = input_tokens - cache_read_tokens

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No unit tests cover the new cache-token logic in _process_event

The existing test file for this module only exercises transformation.py; streaming_iterator.py has no test coverage for the usage-processing path. The PR's test-plan checkboxes are all unchecked, so there is no automated verification that the OpenAI-path fix, the Anthropic fallback, or the subtraction guard behaves correctly. A small parametrised unit test over mock usage objects with input_tokens_details, cache_read_input_tokens, or both absent would prevent regressions here.

Rule Used: What: Ensure that any PR claiming to fix an issue ... (source)

@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: This PR is currently BLOCKED from merge.

Score: 2/5

Why blocked:

  • 1 PR-related CI failure (Greptile gate: score 3/5 below required 4/5 — request a Greptile review (@greptileai) and resolve its comments before maintainer review.) (pr_related_failures, -2 pts)
  • Greptile 3/5 (greptile_low, -1 pts)

Details: Score docked for: 1 PR-related CI failure (Greptile gate: score 3/5 below required 4/5 — request a Greptile review (@greptileai) and resolve its comments before maintainer review.); Greptile 3/5.

Fix the issues above and push an update — the bot will re-review automatically.

Note: This bot is still in beta and might not always work as expected. Please share any feedback via Slack.

@Anai-Guo

Copy link
Copy Markdown
Contributor Author

@greptileai please review — flagged as missing Confidence Score by litellm-agent.

@Anai-Guo
Anai-Guo changed the base branch from main to litellm_internal_staging May 28, 2026 01:12
@Anai-Guo

Copy link
Copy Markdown
Contributor Author

Closing in favor of #32445, which addresses the same issue (#28354) more completely — it patches both streaming_iterator.py and transformation.py with fuller test coverage, whereas this PR only touched the streaming iterator and has since gone stale (conflicting). Thanks @silencedoctor for the more thorough fix.

@Anai-Guo Anai-Guo closed this Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Anthropic→Responses streaming adapter never extracts cache_read_input_tokens (always 0)

1 participant