test(e2e): pin prompt-cache, service-tier, and cost-header billing as permanent regressions - #37607
Merged
mateo-berri merged 4 commits intoAug 20, 2026
Merged
Conversation
Seven live e2e tests covering cost-tracking regressions that currently ship unnoticed: cache-write tokens billed at the cache-creation rate (#34046), per-component cost_breakdown on the spend row (#31686), cache reads billed at the cache-read discount on streamed calls (#34812), cache tokens surviving the anthropic-messages to Responses bridge (#34957), priority-tier rates applied to input, output and reasoning (#35923, #35925), the per-component response cost headers summing to the total (#36965), and cost injected into the final usage frame of an /openai passthrough stream (#36503). Every test registers its own deployment with a distinct custom rate per component, so a component billed at the wrong rate cannot pass. The shared helpers in cost_rows.py encode the one thing the two surfaces disagree on: the spend row's input_cost is gross of cache while the response's cost-input header is net of it.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
Greptile SummaryThis test-only PR adds live regression coverage for prompt-cache accounting, service-tier pricing, and additive response-cost headers.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| tests/e2e/quota_management/spend_tracking/cost_rows.py | Adds shared typed spend-row models, polling helpers, priced-model registration, and component-cost assertions. |
| tests/e2e/quota_management/spend_tracking/test_cache_cost_accounting_e2e.py | Adds live regression tests for cache-write, cache-read, streaming, and Messages-bridge billing behavior. |
| tests/e2e/quota_management/spend_tracking/test_cost_headers_e2e.py | Verifies that response-cost component headers use configured rates and sum to the total. |
| tests/e2e/quota_management/spend_tracking/test_service_tier_pricing_e2e.py | Verifies priority-tier input, output, and reasoning costs against explicit deployment rates. |
| tests/e2e/models.py | Additively extends e2e models with Messages usage and optional cache and priority pricing fields. |
| tests/e2e/coverage_registry/quota_management.yaml | Registers the newly covered spend-tracking behaviors and one explicitly uncovered passthrough-stream gap. |
Reviews (2): Last reviewed commit: "test(e2e): request reasoning explicitly ..." | Re-trigger Greptile
…g flag The final streaming usage frame only carries usage.cost when the proxy runs with litellm_settings.include_cost_in_streaming_usage: true, and that flag is readable only off the module-level litellm setting. There is no header, key, or management route that turns it on per request, so a test cannot ask the shared e2e proxy for it, and the proxy's config does not live in this repo. The registry row stays as an uncovered gap with the reason recorded, rather than being deleted, so the behavior is still on the list of things we want covered once the gateway config is reachable. The StreamOptions model, ChatBody.stream_options, Usage.cost, and AnthropicMessagesResponse.id existed only for that test, so they go with it.
The two tests that assert on reasoning cost read reasoning_tokens off the response and required it to be nonzero, without ever asking the model to reason. Both now send reasoning_effort, so the assertion rests on a parameter the test sets rather than on the model's default behavior. The cache-breakdown test sends it on its prime call too: OpenAI's prefix cache keys on the reasoning setting as well as the tokens, so priming at a different effort never produces a read.
Contributor
Author
Contributor
Author
|
bugbot run |
Contributor
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 975a680. Configure here.
mateo-berri
enabled auto-merge
August 20, 2026 22:53
yucheng-berri
approved these changes
Aug 20, 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.
TLDR
Problem this solves:
How it solves it:
User Flow
Before: back when these regressions were live, a customer running a long cached prompt through the gateway read back a bill that was wrong in several places at once, and nothing in the suite would have caught any of them coming back
"cache_write_tokens": 3627come back underusage.prompt_tokens_details"cached_tokens": 2816back, and the row still shows no cache-read cost, so the discounted tokens bill at full input price"stream": trueand the usage comes back with the cached-token count dropped, so a streamed cache hit costs the same as a cold one"cache_read_input_tokens": 7936but the spend row records zero cached tokens"service_tier": "priority", the reply confirms"service_tier": "priority", and the reasoning tokens inside the answer are still billed at the standard output ratex-litellm-response-cost-*headers off any of those replies and the per-component values do not add up tox-litellm-response-costAfter: every one of those readings is right today, and this PR is what holds them there, since each step above is now asserted by a live test that goes red if the number moves
"cache_write_tokens": 3627"cache_read_input_tokens": 7936shows up on the spend row and is billed at the cache-read rate"service_tier": "priority"x-litellm-response-cost-*component headers add up exactly tox-litellm-response-costRelevant issues
Pins the fixes for #34046, #31686, #34812, #34957, #35923, #35925, and #36965
Linear ticket
Resolves LIT-5869
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
This PR adds tests only, so no product code differs between the two sides. Before is the merge base showing the behaviors have no coverage; After is the live run at the tip proving the numbers those tests assert are the numbers a real proxy produces
Shared setup: a proxy from this branch on
http://localhost:48206against a local Postgres, and four deployments registered at runtime, each with rates ~100x canonical and distinct per component so a bill computed from the wrong rate cannot match: input4e-05, output8e-05, cache-read1e-05, cache-creation5e-05, and on the tier deployment priority input6e-05and priority output1.6e-04. All calls use a virtual key fromPOST /key/generate, and all traffic hits real OpenAI. The two legs that assert on reasoning cost send"reasoning_effort": "high", the same as the tests doBefore (0b37454)
uv run pytest tests/e2e/quota_management/spend_tracking --collect-only -q | tail -3-> the suite collects nothing for prompt-cache billing, service-tier pricing, cost headers, or passthrough stream costpython -m coverage_registry.collector-> the six registry rows added here are absent, so none of these behaviors counts as covered and a regression in any of them ships with a green suiteAfter (975a680)
Case 1: cache-write tokens bill at the cache-creation rate (#34046)
curl -s $P/chat/completions -H "Authorization: Bearer $KEY" -d @qa3_write.json->id chatcmpl-EEt11sR342HaQnD4X7rd2g7a9AKCA,"prompt_tokens": 3630,"cache_write_tokens": 3627curl -s "$P/spend/logs?request_id=chatcmpl-EEt11sR342HaQnD4X7rd2g7a9AKCA" -H "Authorization: Bearer sk-1234"->"cache_creation_cost": 0.18135(3627 x 5e-05),"input_cost": 0.18147, so the 3 fresh tokens are the remaining0.00012at 4e-05, and"total_cost": 0.18395equals input plus the0.00248outputCase 2: the breakdown itemizes cache-read and reasoning (#31686)
curl -s $P/chat/completions ... -d @qa10_read_prime.jsonthen-d @qa10_read_measure.json, both bodies carrying"reasoning_effort": "high"-> the measured call returns"cached_tokens": 2816,"reasoning_tokens": 67curl -s "$P/spend/logs?request_id=chatcmpl-EEuyXBfTmaempDNVgnKQ16C4nB7TU" ...->"cache_read_cost": 0.02816(2816 x 1e-05),"reasoning_cost": 0.00536(67 x 8e-05) sitting inside"output_cost": 0.00624, and"input_cost": 0.0608less the cache-read cost leaves the 816 fresh tokens at 4e-05Priming at a different
reasoning_effortthan the measured call produces no cache read at all, so both bodies carry the same value. That is why the test sends it on its prime call tooCase 3: a streamed cache read keeps its discount (#34812)
curl -s $P/chat/completions -H "Authorization: Bearer $KEY" -d @qa9_prime.jsonprimes a fresh 1200-word prefix ->cached_tokens 0curl -sN $P/chat/completions -H "Authorization: Bearer $KEY" -d @qa9_stream.jsonsends the same prefix with"stream": trueand nostream_options-> first chunkid chatcmpl-EEtJmzT2FmyV0Db1tP6Bh2jj2GVvscurl -s "$P/spend/logs?request_id=chatcmpl-EEtJmzT2FmyV0Db1tP6Bh2jj2GVvs" -H "Authorization: Bearer sk-1234"->"cache_read_input_tokens": 2816survived the stream,"cache_read_cost": 0.02816(2816 x 1e-05), and the 815 fresh tokens are0.0326at 4e-05, so"input_cost": 0.06076is exactly those two added together and"total_cost": 0.06212is that plus the0.00136output. At full input price this call would have billed0.14524, so the discount is worth 2.3x hereOpenAI's implicit caching is best-effort, so this leg was driven with the same retry-with-a-fresh-prefix loop the test uses; it hit on the first attempt above, and a separate control run put a plain stream and an
include_usagestream on one warm prefix to confirm the discount does not depend on the client asking for usageCase 4: cache tokens survive the /v1/messages bridge onto a Responses-only model (#34957)
curl -s $P/v1/messages -H "Authorization: Bearer $KEY" -d @qa3_bridge_prime.json->"input_tokens": 9030curl -s $P/v1/messages -H "Authorization: Bearer $KEY" -d @qa3_bridge_read.json->"input_tokens": 1095,"cache_read_input_tokens": 7936curl -s "$P/spend/logs?api_key=$KEY" ...-> the bridged row carriescache_read7936 with"cache_read_cost": 0.07936and an"input_cost"whose remainder after the cache read is the fresh tokens at 4e-05Case 5: priority tier bills every component at the priority rate, reasoning included (#35923, #35925)
curl -s $P/chat/completions -H "Authorization: Bearer $KEY" -d @qa10_tier.json->"service_tier": "priority","prompt_tokens": 34,"completion_tokens": 51,"reasoning_tokens": 40curl -s "$P/spend/logs?request_id=chatcmpl-EEuyiqkNSoFQX30mN5FdHZQLBXlcb" ...->"service_tier": "priority"on the row,"input_cost": 0.00204(34 x 6e-05),"output_cost": 0.00816(51 x 1.6e-04), and"reasoning_cost": 0.0064(40 x 1.6e-04). The standard rates would have billed 0.00136 and 0.00408, so none of the three landed on the default tierCase 6: the per-component cost headers add up to the total (#36965)
curl -s -D- $P/chat/completions -H "Authorization: Bearer $KEY" -d @qa10_read_measure.json->x-litellm-response-cost: 0.06704,-input: 0.03264,-output: 0.00624,-cache-read: 0.02816,-tool-usage: 0.00.03264 + 0.00624 + 0.02816 + 0.0 = 0.06704, equal to the total header. Note the header's input component is net of cache while the spend row'sinput_costis gross (0.0608on the same call), which is why the two surfaces are asserted differentlyAll six tests also ran green against this same proxy at this commit:
6 passed in 139.10sType
✅ Test
Caveats (if any)
Two behaviors from the same batch were investigated and deliberately left out, both because no live proxy can reach them:
POST /model/newandPOST /model/updatebind the body to a model whose cost fields are typedfloat | None, so pydantic coerces"3e-06"at the HTTP boundary, and the boot-config path is coerced too before it is ever read. The PR's own unit tests have to patch the reader to inject a string, which the e2e harness forbids/v1/responses(fix: report real token usage on guardrail-blocked /v1/responses replies #36907) only differs from the old behavior on a post-call block that carries the upstream response. Every guardrail available in-tree either answers 400 outright or blocks pre-call with no upstream response, which yields zeros both before and after the fix. Reaching the fixed branch needs Bedrock withdisable_exception_on_block, Straiker, or Rubrik, all external and credentialedA seventh registry row,
quota_management.spend_tracking.passthrough_stream.injects_usage_cost, ships uncovered on purpose. The behavior it names (#36503) only shows up withlitellm_settings.include_cost_in_streaming_usageturned on, and that is a boot-time setting the shared e2e stack does not enable today, so a test written against that stack now cannot reach the branch it is meant to pin. That is observed rather than assumed: an earlier commit on this branch did carry such a test, and it came back from CI withcost=Noneon the final usage frame, which is exactly the pre-fix reading. A change to the stack's configuration is open and has to land first. The row stays in the registry so the collector keeps reporting it as a gap instead of letting the behavior quietly disappear from the denominatorOne doc nit found on the way, not changed here: the
input_costfield comment inlitellm/types/utils.pyreads "Cost of raw (non-cached) input tokens only", which is true of the response header but not of the spend row, where the same name is gross of cacheQA runbook
tests/e2e/quota_management/spend_tracking/test_cache_cost_accounting_e2e.py::TestCacheCostAccounting::test_cache_write_tokens_billed_at_cache_creation_rate - a fresh ~3.6k-token prompt's cache-write tokens bill at the cache-creation rate, not the input rate
openai/gpt-5.6-lunaandinput_cost_per_token4e-05,cache_creation_input_token_cost5e-05cache_creation_costto equal those tokens times 5e-05, withinput_costminus that leaving the fresh tokens at 4e-05tests/e2e/quota_management/spend_tracking/test_cache_cost_accounting_e2e.py::TestCacheCostAccounting::test_cost_breakdown_reports_component_costs - the row itemizes cache-read and reasoning cost, with reasoning a subset of output
openai/gpt-5.5and the four distinct ratescache_read_costat 1e-05 per cached token,reasoning_costat 8e-05 per reasoning token, andreasoning_costno greater thanoutput_costtests/e2e/quota_management/spend_tracking/test_cache_cost_accounting_e2e.py::TestCacheCostAccounting::test_streaming_cache_read_billed_at_cache_read_rate - a streamed cache hit gets the same discount as a non-streamed one
"stream": truetests/e2e/quota_management/spend_tracking/test_cache_cost_accounting_e2e.py::TestCacheCostAccounting::test_messages_bridge_keeps_cache_tokens - cache tokens survive the anthropic-messages to Responses bridge and keep their discount
openai/gpt-5.3-codex, which is Responses-only and so forces the bridgecache_read_input_tokensabove zero on the secondcache_read_costat 1e-05 each (the bill is filed under a different id than the response's, so query by key)tests/e2e/quota_management/spend_tracking/test_service_tier_pricing_e2e.py::TestServiceTierPricing::test_priority_tier_bills_priority_rates - a priority call bills input, output, and reasoning at the priority rates
input_cost_per_token_priority6e-05,output_cost_per_token_priority1.6e-04"service_tier": "priority"and a reasoning question, and expect the response to echo"service_tier": "priority"service_tierpriority on the row, output and reasoning at 1.6e-04, input at 6e-05tests/e2e/quota_management/spend_tracking/test_cost_headers_e2e.py::TestCostHeaders::test_component_cost_headers_sum_to_total - the per-component cost headers add up to the total header
openai/gpt-5.5and the same four distinct rates-D-so the measured call's headers are visible (the test reads them off the raw transport, which the typed client drops)x-litellm-response-costand the-input,-cache-read,-cache-creation,-output,-tool-usagecomponentsinput_cost)CI note, so the red gate is not mistaken for this diff: every e2e run at this tip came back
1 failed, 640 passed, 58 skipped, and the one failure isllm_translation/test_audio_transcriptions_e2e.py::TestAudioTranscriptions::test_missing_model_returns_error, which fails the same way on every PR that runs the suite today. All six tests added here passed against the shared stack. #37565 carries the one-line assertion fix for that test, so this gate goes green once it merges. Separately,buildkite/e2e-testsis not one oflitellm_internal_staging's required checks, so the red X does not block mergingFinal Attestation
CHECKED: live-pr-risk at 975a680. Five of the six files are new or registry-only, and the one shared file is
tests/e2e/models.py, where every change is additive:AnthropicMessagesResponse.usageplus four optional cost fields onLiteLLMParamsBody, all defaulting to None, with no field renamed, retyped, or given a new default. Request bodies serialize withexclude_none=True, so no existing suite's call changes on the wire, and no suite outside this PR reads.usageoff a/v1/messagesresponse. The full e2e suite collects clean on the head (695 of 697, two deselected, 0 errors), the coverage collector runs--strictwithout a failure, basedpyright overtests/e2ereports 0 errors, and all six cases were driven live against a real proxy, database, and OpenAINote
Low Risk
Tests-only; no product billing code changes. Residual risk is flaky live OpenAI prefix-cache and spend-log polling, not a production behavior change.
Overview
Pins six shipped spend-tracking fixes with live OpenAI e2e tests that register per-deployment custom rates so a wrong-rate bill cannot pass.
Cache accounting now asserts cache-write tokens bill at the cache-creation rate, spend-row
cost_breakdownitemizes cache-read/creation/reasoning, streamed usage keeps the cache-read discount, and/v1/messagesthrough a Responses-only model keeps cached tokens on the bill.Priority
service_tierbills input, output, and reasoning at*_priorityrates and records the tier on the spend row.x-litellm-response-cost-*headers must sum to the total, with input net of cache (unlike the row’s grossinput_cost).Coverage registry rows are added for those behaviors; passthrough stream cost is registered as an intentional gap until the shared stack enables
include_cost_in_streaming_usage.Reviewed by Cursor Bugbot for commit 975a680. Bugbot is set up for automated code reviews on this repo. Configure here.