fix(bedrock): read batch usage by payload shape, not by provider name - #37078
Conversation
Every bedrock batch output line went through the Anthropic usage parser, which reads snake_case input_tokens/output_tokens. Converse-family models (Nova and friends) report camelCase inputTokens/outputTokens, so their usage came back 0/0/0 and the batch billed $0 despite real token consumption. Usage is now selected by the shape of the payload: a Converse-shaped block goes through the same transform the live Converse path uses, so a batch and an equivalent non-batch call agree on tokens, including cache reads and writes. Anthropic-shaped bedrock output is unchanged. A shape neither parser understands (an InvokeModel-native payload from Titan, Cohere, or Llama, which name their counts differently again) still reads zero, but now warns with the keys it saw instead of silently billing $0. Exposes the Converse usage transform as public, since batch parsing is a second legitimate caller; that also removes the private-member access invoke_handler was already making.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryBedrock batch usage is now selected by payload shape, with Converse-shaped counts normalized through the live Converse usage transform and unknown shapes logged.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/batches/batch_utils.py | Selects Converse normalization for camelCase Bedrock usage payloads while preserving Anthropic parsing and warning on unknown shapes. |
| litellm/llms/bedrock/chat/converse_transformation.py | Adds reusable Converse batch-usage normalization and exposes the usage transform for batch and streaming callers. |
| litellm/llms/bedrock/chat/invoke_handler.py | Updates streaming usage processing to call the newly public Converse transform. |
| tests/test_litellm/batches/test_batch_utils.py | Covers Converse and Anthropic batch payloads, cache-token accounting, inferred totals, and unknown-shape warnings. |
| tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py | Covers shape classification, incomplete batch blocks, cache-token handling, and streaming usage conversion. |
Reviews (3): Last reviewed commit: "refactor(bedrock): own the Converse batc..." | Re-trigger Greptile
… layer Shape detection and block normalization sat in the generic batch layer, which let batch and live parsing of the same wire format drift apart. Both now live on AmazonConverseConfig as is_converse_usage_shape and usage_from_batch_output, so batch_utils asks the provider adapter rather than knowing Bedrock's field names. Adds direct coverage for the shape predicate, the completion of an incomplete block, cache-count inflation, and the streaming usage event that shares the public transform. Drops the narrative banner from the batch tests.
|
Both addressed. Shape detection and block normalization now live on AmazonConverseConfig as is_converse_usage_shape and usage_from_batch_output, so the batch layer asks the provider adapter instead of knowing Bedrock field names, and batch and live parsing cannot drift. Banner comment and redundant docstrings removed. Also added direct provider-layer coverage for the predicate, the incomplete-block completion, cache-count inflation, and the streaming usage event, which closes the uncovered line Codecov flagged in invoke_handler. |
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5fe7793. Configure here.
mateo-berri
left a comment
There was a problem hiding this comment.
LGTM. Thanks for the contribution!
3f4810b
into
BerriAI:litellm_internal_staging

TLDR
Problem this solves:
How it solves it:
User Flow
Before: a team running Nova batch jobs sees every completed batch logged with zero tokens and $0.00 spend, so the work appears free and never reaches chargeback
purpose: batch,target_model_names: amazon.nova-lite) and get back a gateway file idinput_file_idand get back a batch id withstatus: validatingstatus: completedAfter: the same batch reports the tokens its own output file shows, and bills accordingly
status: validatingstatus: completedRelevant issues
Linear ticket
Resolves LIT-5668
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Two live Bedrock managed batch runs against real AWS in us-west-2, one per checkpoint, each on its own proxy booted from a fresh Postgres with the batch cost job polling every 30s. Model
bedrock/us.amazon.nova-lite-v1:0(Converse family), the same 110-record JSONL both times, real $$$Each leg is the exact end-user flow:
Before, at merge base 973329e: the batch completes and Bedrock's own output file reports real usage on every one of the 110 lines, for example
"usage":{"inputTokens":8,"outputTokens":11,"totalTokens":19}, summing to 990 input, 1597 output, and 2587 total tokens, yet the tracked spend row reads zero across the boardAfter, at PR tip 5fe7793: same flow, same model, same input file. The output file sums to 990 input, 1577 output, and 2567 total tokens, and the spend row now matches it token for token, priced at the 50% batch rate on nova-lite's on-demand cost
The after leg logged zero "billed at $0" warnings, confirming the Converse shape is now parsed rather than warned about. Anthropic-shaped Bedrock batches were additionally verified unaffected across 23 real batches on a deployed gateway, where token counts reconciled exactly against the providers' own output files
Type
🐛 Bug Fix
Changes
Bedrock batch usage was parsed by provider name rather than by the shape of the payload, and the single parser chosen reads Anthropic's snake_case token counts. Any Converse-family model, Nova being the common one, reports camelCase counts instead, so usage aggregated to 0/0/0 and the batch priced at $0 despite real consumption
Usage is now selected by the shape it actually has. A Converse-shaped block goes through the same transform the live Converse path uses, so a batch and an equivalent non-batch call agree on tokens, including cache reads and cache writes. Anthropic-shaped output keeps its existing parser, and the OpenAI-shaped path for other providers is untouched
Reusing the live transform means also exposing it: this makes the Converse usage transform public, since batch parsing is a second legitimate caller from outside that module. That additionally removes the private-member access
invoke_handlerwas already making on it, which is why the basedpyright budget ratchets down by one hereA payload neither parser understands, such as an InvokeModel-native shape from Titan, Cohere, or Llama, still reads zero. That is left as is rather than guessed at, but it no longer does so quietly: it warns with the usage keys it saw, so the next unhandled shape shows up in logs instead of as a silently free batch
Caveats
Final Attestation