Skip to content

fix(responses): map cache_write_tokens to cache_creation_input_tokens in usage transform - #33071

Closed
michelligabriele wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_fix_responses_cache_write_tokens
Closed

fix(responses): map cache_write_tokens to cache_creation_input_tokens in usage transform#33071
michelligabriele wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_fix_responses_cache_write_tokens

Conversation

@michelligabriele

Copy link
Copy Markdown
Collaborator

Relevant issues

No existing issue tracks the Responses-path cache_write_tokens drop. Related (same GPT-5.6 caching surface, request side): #32656 / #32669 — those cover prompt_cache_breakpoint passthrough; this PR covers the usage/cost side.

Linear ticket

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests) — local run of the full lint job (ruff + format + strict gates, basedpyright gate, type-discipline, circular-imports, import-safety) and the affected unit suites are green; CircleCI running on the branch
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

Setup: live proxy, model openai/responses/gpt-5.6-luna (real OpenAI calls), built-in cost-map pricing (input 1e-06, cache write 1.25e-06, cache read 1e-07, output 6e-06), Postgres spend logging.

Before fix (base c2141b1113): the transform rebuilds prompt_tokens_details from a fixed field set and never reads cache_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/completions with a fresh ~3.5k-token prompt (≥1024-token prefix → automatic cache write):

"usage": {"completion_tokens": 5, "prompt_tokens": 3541, "total_tokens": 3546,
 "completion_tokens_details": {"reasoning_tokens": 0},
 "prompt_tokens_details": {"cached_tokens": 0, "cache_creation_tokens": 3538},
 "cache_creation_input_tokens": 3538}

Same request repeated (cache read):

"usage": {"completion_tokens": 5, "prompt_tokens": 3541, "total_tokens": 3546,
 "prompt_tokens_details": {"cached_tokens": 3538, "cache_creation_tokens": 0},
 "cache_creation_input_tokens": 0}

GET /spend/logs for the two calls:

  • write call: "spend": 0.0044555 = 3×1e-06 + 3538×1.25e-06 + 5×6e-06 (exact)
  • read call: "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": 0 explicitly 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_usage now extracts input_tokens_details.cache_write_tokens (dict and typed-object branches) and passes it as cache_creation_input_tokens into Usage(), which sets prompt_tokens_details.cache_creation_tokens, the _cache_creation_input_tokens mirror, 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_tokens is deliberately NOT inflated: OpenAI reports details as subsets of input_tokens (Anthropic reports additively).
  • litellm/types/llms/openai.py — declare cache_write_tokens: Optional[int] on InputTokensDetails (was only surviving as an untyped extra).
  • litellm/responses/litellm_completion_transformation/transformation.py — reverse bridge symmetry: prompt_tokens_details.cache_creation_tokensinput_tokens_details.cache_write_tokens for providers bridged into /v1/responses.
  • litellm/litellm_core_utils/llm_cost_calc/utils.py_get_token_base_cost falls back to the base input rate when cache_creation_input_token_cost is absent, so models without write pricing bill writes at base rate instead of $0 (mirrors the reasoning-rate precedent; explicit 0.0 pricing still wins; tiered/above-threshold and service-tier lookups unchanged).
  • Tests — forward transform (dict + object input, no-field unchanged), end-to-end cost with builtin gpt-5.6 pricing (fails without the fix), base-rate fallback with custom pricing, reverse bridge mapping.

Known follow-up (out of scope): DB models keyed openai/openai/responses/<model> still don't resolve built-in cost-map pricing (register_model warning); the base-rate fallback removes the $0-billing consequence in the meantime.

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/responses/utils.py 66.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a cost-accounting gap on the OpenAI Responses API path: cache_write_tokens (the GPT-5.6 field for paid prompt-cache writes) was never mapped to litellm's internal cache_creation_input_tokens, causing those tokens to be billed at the base input rate or — for models without an explicit write-cost entry — at $0.

  • litellm/responses/utils.py: extracts cache_write_tokens from both the dict and typed-object branches of input_tokens_details and passes it as cache_creation_input_tokens to Usage(), which sets prompt_tokens_details.cache_creation_tokens, _cache_creation_input_tokens, and the top-level field in one shot.
  • litellm/litellm_core_utils/llm_cost_calc/utils.py: changes the cache_creation_cost default from 0.0 to the base input rate when cache_creation_input_token_cost is absent from the model's pricing entry; explicit 0.0 pricing is preserved since None (absent) is distinguished from 0.0.
  • litellm/responses/litellm_completion_transformation/transformation.py: adds the reverse bridge so prompt_tokens_details.cache_creation_tokens round-trips back to input_tokens_details.cache_write_tokens for providers bridged into /v1/responses.

Confidence Score: 4/5

The 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.

Important Files Changed

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

Comment on lines +3125 to +3135
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="")

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 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.

Suggested change
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="")

Comment on lines +228 to +229
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

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 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!

@codspeed-hq

codspeed-hq Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_fix_responses_cache_write_tokens (14537d2) with litellm_internal_staging (f1f0a0b)

Open in CodSpeed

@Classic298

Copy link
Copy Markdown
Contributor

.

@michelligabriele
michelligabriele force-pushed the litellm_fix_responses_cache_write_tokens branch from f69faf0 to 14537d2 Compare July 22, 2026 12:55
@shivamrawat1

Copy link
Copy Markdown
Collaborator

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

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.

3 participants