fix(responses): map cache_write_tokens to cache_creation_input_tokens in usage transform - #33071
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR fixes a cost-accounting gap on the OpenAI Responses API path:
Confidence Score: 4/5The fix is well-scoped and tested; the main risk is the silent billing change for custom/DB-registered models that report cache write tokens without explicit write pricing. The core forward-transform and reverse-bridge logic is correct and covered by targeted unit tests. The cache_creation_cost fallback to prompt_base_cost is intentional and correctly distinguishes an absent price (None) from an explicit zero (0.0). The global state mutation in one new test (litellm.model_cost) is not restored, which can silently affect test isolation but does not affect production behaviour. litellm/litellm_core_utils/llm_cost_calc/utils.py — the fallback-to-base-input-rate change deserves a second look to confirm it won't surprise operators of custom-priced or DB-registered models that have cache write tokens but no explicit write-cost entry.
|
| Filename | Overview |
|---|---|
| litellm/responses/utils.py | Extracts cache_write_tokens from both dict and object branches of input_tokens_details and passes it as cache_creation_input_tokens to Usage(); logic is correct and consistent across both code paths. |
| litellm/litellm_core_utils/llm_cost_calc/utils.py | Changes cache_creation_cost default from 0.0 to the base input rate when the cache_creation_input_token_cost key is absent; intentional bug fix but silently changes billing behaviour for any model that reports cache write tokens without explicit write pricing. |
| litellm/responses/litellm_completion_transformation/transformation.py | Adds reverse-bridge mapping: prompt_tokens_details.cache_creation_tokens → input_tokens_details.cache_write_tokens; symmetric with the forward transform and correctly scoped. |
| litellm/types/llms/openai.py | Adds cache_write_tokens: Optional[int] = None to InputTokensDetails; formalises a field that was already accepted as an untyped extra due to extra="allow". |
| tests/test_litellm/test_cost_calculator.py | Two new cost tests: end-to-end gpt-5.6-luna pricing (relies on gpt-5.6-luna being present in local cost map) and custom-pricing fallback to base rate; both are pure mock tests but the first test modifies litellm.model_cost global state without teardown. |
| tests/test_litellm/responses/test_responses_utils.py | Adds three new unit tests covering dict input, object input, and no-field-unchanged cases; all are pure mock tests with no real network calls. |
| tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py | Adds test for the reverse bridge (cache_creation_tokens → cache_write_tokens); uses mock objects, no network calls. |
Reviews (1): Last reviewed commit: "fix(responses): map cache_write_tokens t..." | Re-trigger Greptile
| def test_responses_usage_cache_write_tokens_billed_at_cache_creation_rate(): | ||
| """ | ||
| GPT-5.6 reports paid cache writes as input_tokens_details.cache_write_tokens | ||
| on the Responses API. The Responses->Chat usage transform must map them to | ||
| cache_creation_tokens so cost calc bills them at | ||
| cache_creation_input_token_cost (1.25x input) instead of the base rate. | ||
| """ | ||
| from litellm.responses.utils import ResponseAPILoggingUtils | ||
|
|
||
| os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" | ||
| litellm.model_cost = litellm.get_model_cost_map(url="") |
There was a problem hiding this comment.
The test sets
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] and overwrites litellm.model_cost but never restores either one after the test completes. Because pytest runs tests in the same process, any subsequent test that calls litellm.completion_cost without its own model-cost setup will silently use this modified global, making failures in later tests difficult to trace.
| def test_responses_usage_cache_write_tokens_billed_at_cache_creation_rate(): | |
| """ | |
| GPT-5.6 reports paid cache writes as input_tokens_details.cache_write_tokens | |
| on the Responses API. The Responses->Chat usage transform must map them to | |
| cache_creation_tokens so cost calc bills them at | |
| cache_creation_input_token_cost (1.25x input) instead of the base rate. | |
| """ | |
| from litellm.responses.utils import ResponseAPILoggingUtils | |
| os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" | |
| litellm.model_cost = litellm.get_model_cost_map(url="") | |
| def test_responses_usage_cache_write_tokens_billed_at_cache_creation_rate(): | |
| """ | |
| GPT-5.6 reports paid cache writes as input_tokens_details.cache_write_tokens | |
| on the Responses API. The Responses->Chat usage transform must map them to | |
| cache_creation_tokens so cost calc bills them at | |
| cache_creation_input_token_cost (1.25x input) instead of the base rate. | |
| """ | |
| from litellm.responses.utils import ResponseAPILoggingUtils | |
| original_model_cost = litellm.model_cost | |
| original_env = os.environ.get("LITELLM_LOCAL_MODEL_COST_MAP") | |
| try: | |
| os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" | |
| litellm.model_cost = litellm.get_model_cost_map(url="") |
| cache_creation_cost_from_map = _get_cost_per_unit(model_info, cache_creation_cost_key, None) | ||
| cache_creation_cost = cache_creation_cost_from_map if cache_creation_cost_from_map is not None else prompt_base_cost |
There was a problem hiding this comment.
Implicit billing change for models without explicit write pricing. Any model that (a) reports
cache_write_tokens > 0 and (b) has no cache_creation_input_token_cost in its pricing entry will silently start being billed at the base input rate instead of $0. This is the intended fix for GPT-5.6, but it also affects custom-priced or DB-registered models (e.g. openai/openai/responses/<model>) where operators may not have added a write-cost field. The explicit-0.0 case is safe since _get_cost_per_unit returns a float for zero values (distinguished from None), but operators relying on the old "writes are free unless priced" behaviour will see a cost change on upgrade without any configuration flag to opt out.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
. |
f69faf0 to
14537d2
Compare
|
Thanks for the PR. This was superseded by #34046, which maps cache_write_tokens into cache creation billing on the Responses path and shipped in v1.95.0, so I'm closing this one |
Relevant issues
No existing issue tracks the Responses-path
cache_write_tokensdrop. Related (same GPT-5.6 caching surface, request side): #32656 / #32669 — those coverprompt_cache_breakpointpassthrough; this PR covers the usage/cost side.Linear ticket
Pre-Submission checklist
Screenshots / Proof of Fix
Setup: live proxy, model
openai/responses/gpt-5.6-luna(real OpenAI calls), built-in cost-map pricing (input1e-06, cache write1.25e-06, cache read1e-07, output6e-06), Postgres spend logging.Before fix (base
c2141b1113): the transform rebuildsprompt_tokens_detailsfrom a fixed field set and never readscache_write_tokens— usage on this path carries no cache-creation fields, and cost calc bills the written tokens at the base input rate: 3541×1e-06 + 5×6e-06 = 0.003571 for the call below (computed pre-fix outcome for the same usage; ~25% undercharge vs the OpenAI invoice).After fix (captured at
f69faf0818):POST /v1/chat/completionswith a fresh ~3.5k-token prompt (≥1024-token prefix → automatic cache write):Same request repeated (cache read):
GET /spend/logsfor the two calls:"spend": 0.0044555= 3×1e-06 + 3538×1.25e-06 + 5×6e-06 (exact)"spend": 0.0003868= 3×1e-06 + 3538×1e-07 + 5×6e-06 (exact)Pre-5.6 control (
openai/responses/gpt-4o-mini, same bridge):"spend": 0.0005325= 3542×1.5e-07 + 2×6e-07 — pure base rate, no phantom premium.Raw provider payload check (direct
POST https://api.openai.com/v1/responses): OpenAI sends"cache_write_tokens": 0explicitly even on pre-5.6 models — the transform passes it through faithfully, and subset semantics (cached + writes ≤ input_tokens) were confirmed on the live payloads.Type
🐛 Bug Fix
Changes
litellm/responses/utils.py—_transform_response_api_usage_to_chat_usagenow extractsinput_tokens_details.cache_write_tokens(dict and typed-object branches) and passes it ascache_creation_input_tokensintoUsage(), which setsprompt_tokens_details.cache_creation_tokens, the_cache_creation_input_tokensmirror, and the top-level field in one shot — covering client response, all logging callbacks, cost calc, spend DB, streaming + non-streaming through the one shared choke point.prompt_tokensis deliberately NOT inflated: OpenAI reports details as subsets ofinput_tokens(Anthropic reports additively).litellm/types/llms/openai.py— declarecache_write_tokens: Optional[int]onInputTokensDetails(was only surviving as an untyped extra).litellm/responses/litellm_completion_transformation/transformation.py— reverse bridge symmetry:prompt_tokens_details.cache_creation_tokens→input_tokens_details.cache_write_tokensfor providers bridged into/v1/responses.litellm/litellm_core_utils/llm_cost_calc/utils.py—_get_token_base_costfalls back to the base input rate whencache_creation_input_token_costis absent, so models without write pricing bill writes at base rate instead of $0 (mirrors the reasoning-rate precedent; explicit0.0pricing still wins; tiered/above-threshold and service-tier lookups unchanged).Known follow-up (out of scope): DB models keyed
openai/openai/responses/<model>still don't resolve built-in cost-map pricing (register_modelwarning); the base-rate fallback removes the $0-billing consequence in the meantime.Final Attestation