fix: stop subtracting cache tokens from Bedrock Converse input_tokens (cherry-pick of upstream #832) - #29
Merged
Conversation
…mne#832) Fixes crmne#828. ### Problem `Converse::Chat#input_tokens` subtracts `cacheReadInputTokens` and `cacheWriteInputTokens` from `inputTokens`. But AWS Bedrock's `inputTokens` is **already** the non-cached count. The cache buckets are reported separately, not folded in. Per the [AWS prompt-caching docs](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html): > When prompt caching is enabled, the `inputTokens` field represents **only the non-cached input tokens**... `total input tokens = inputTokens + cacheReadInputTokens + cacheWriteInputTokens` So the subtraction removes tokens that were never there, understating input on every cache hit and flooring it to `0` whenever the cached prefix exceeds the fresh input (the common multi-turn case). Any cost or usage tracking on `message.input_tokens` under-reports. ### Why pass-through is correct The `Anthropic` provider already handles the same Claude models correctly: `anthropic/chat.rb` passes `input_tokens` through raw and reads the cache buckets separately. Bedrock serves those same models with the same semantics, so this just brings Converse into line. The same fix landed in LiteLLM ([#15292](BerriAI/litellm#15292)) and was reported in LangSmith ([#1858](langchain-ai/langsmith-sdk#1858)). ### Change - `input_tokens` returns `usage['inputTokens']` unchanged. `cached_tokens` and `cache_creation_tokens` are still parsed separately, so nothing is lost. Streaming delegates to this method, so both paths are covered. - Updated the converse chat spec. The previous test asserted the subtraction on a payload that cannot occur under AWS semantics (`inputTokens` already excludes cache), so its fixture is corrected to a realistic one, and a regression is added for the floor-to-zero case. Verified: `bundle exec rspec spec/ruby_llm/protocols/converse/` plus `spec/ruby_llm/providers/bedrock_spec.rb` green (39 examples), rubocop clean. Co-authored-by: Carmine Paolino <carmine@paolino.me> (cherry picked from commit dc79ce6) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Sam Boland <sam.boland@appfolio.com>
staging-supernova-dx-appf-io
Bot
marked this pull request as ready for review
September 24, 2026 20:10
staging-supernova-dx-appf-io
Bot
requested a review
from a team
as a code owner
September 24, 2026 20:10
Author
|
🤖 Supernova Code Review — View trace — via automation "Code Review Opus 5.5 Medium" (run), installed by Sam Boland (sam.boland@appfolio.com) |
Author
There was a problem hiding this comment.
Reviewed the PR and found nothing I'd ask you to change. No inline comments.
What I checked:
lib/ruby_llm/protocols/converse/chat.rb:input_tokensnow returnsusage['inputTokens']as-is. This matches how AWS reports it:inputTokens,cacheReadInputTokensandcacheWriteInputTokensare separate counts that add up. The old early return for a missinginputTokensis still covered, because the value just comes back asnil.lib/ruby_llm/protocols/converse/streaming.rb(extract_input_tokens, lines 286–289) callsChat.input_tokens, so the streaming path gets the fix too.cached_tokensandcache_creation_tokenswere already copied straight from the response.spec/ruby_llm/protocols/converse/streaming_spec.rbhas no assertions that depended on the old subtraction.- The updated and new examples in
chat_spec.rbtest both cases: normal input with cache tokens, and the case where the cached prefix is larger than the new input and the result used to drop to 0.
— via automation "Code Review Opus 5.5 Medium" (run), installed by Sam Boland (sam.boland@appfolio.com)
samuelboland
approved these changes
Sep 24, 2026
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.
What this does
Fixes double-subtraction of cache tokens from
input_tokensin the Bedrock Converse protocol.RubyLLM::Protocols::Converse::Chat.input_tokenswas computingmax(inputTokens - cacheReadInputTokens - cacheWriteInputTokens, 0), but AWS Bedrock already reportsinputTokensas excluding cached tokens —inputTokens,cacheReadInputTokens, andcacheWriteInputTokensare separate, additive buckets, notinputTokensinclusive of cache. The subtraction removed tokens never counted in the first place and floored the result to0whenever the cached prefix exceeded fresh input, which is the normal case in multi-turn conversations. Production data showed every assistant message with cache tokens recordinginput_tokens = 0because of this bug.This ships upstream's fix from crmne#832 (commit
dc79ce62949d5648a838d4bdffda7499e55186d7), cherry-picked onto this fork's base.input_tokens(usage)now simply returnsusage['inputTokens']as-is, with a comment explaining why (linking AWS's prompt-caching docs). The streaming path (Protocols::Converse::Streaming#extract_input_tokens) delegates directly toChat.input_tokens, so it picks up the fix automatically without any code change. Inspection ofchat.rbandstreaming.rbconfirmed the only other uses ofcacheReadInputTokens/cacheWriteInputTokensare pass-throughs populatingcached_tokens/cache_creation_tokenson messages — no subtraction — so no other Converse code path needed the same fix.The spec in
spec/ruby_llm/protocols/converse/chat_spec.rbwas updated: the old example asserting the subtraction behavior was replaced with one asserting AWS'sinputTokensis exposed as-is (inputTokens=50, cacheReadInputTokens=40, cacheWriteInputTokens=10→input_tokens == 50), and a new example covers the previously-broken case (inputTokens=3, cacheReadInputTokens=7714, cacheWriteInputTokens=327→input_tokens == 3, no longer floored to 0). All other fork-only examples in that file were preserved unchanged.Out of scope: the Mantle Responses protocol's token accounting is deliberately left untouched, since OpenAI's
input_tokensthere is inclusive of cached tokens and the subtraction is correct in that context. The correspondingGemfile.lockbump on the supernova side follows separately and is not part of this PR.Type of change
Scope check
Required for new features
N/A — this is a bug fix, not a new feature.
Quality check
overcommit --installand all hooks passbundle exec rake vcr:record[provider_name]bundle exec rspecmodels.json,aliases.json)bundle exec rspec spec/ruby_llm/protocols/converse/ spec/ruby_llm/providers/bedrock_spec.rbandbundle exec rubocopon the changed files were run and reported passing/clean. No VCR cassette re-recording was needed since no live API interaction changed — this is a purely arithmetic fix. The specs added for this change are precise, covering both the general non-subtraction behavior and the previously-broken high-cache-token case.AI-generated code
API changes
The method name and signature at the
Chat.input_tokensboundary are unchanged; only the internal return-value calculation changed. Behaviorally,input_tokenson Bedrock Converse responses will now report higher, correct values instead of frequently reporting0.