litellm/litellm
tin-berri 79d412efc2
fix: net prompt-caching savings against the cache-write premium (#36452)
* fix: net prompt-caching savings against the cache-write premium

Prompt-caching savings priced only the cache-read discount and ignored what
the provider charges to create the cache entry. Anthropic bills cache writes
at 1.25x the input rate, so a request that writes a large cache and reads
little from it is a net loss that the dashboard reported as a gain -- or, on
a pure cold write, as a flat zero.

The counterfactual the number answers is "what would this have cost with
caching off", where every token is billed at the input rate. Since
prompt_tokens partitions disjointly into text + reads + writes, that gives

    savings = reads * (input - read_rate) - writes * (write_rate - input)

The write term is the premium over the input rate, not the full write cost:
the tokens would have been paid for at the input rate anyway, so only the
markup is attributable to caching.

The premium stays signed rather than clamped. Three models in the pricing map
price writes below input, and clamping would silently drop that saving.
A model with no cache_creation_input_token_cost falls open to the input cost,
yielding a zero premium -- this is why the change is a no-op for the implicit
caching providers (OpenAI, Gemini), which publish no write price, and bites
exactly on Anthropic and Bedrock.

Verified live through the proxy on a mock Anthropic rig across four cases
(cold pure-write, warm pure-read, write-heavy, read-heavy). Reported total
matched the derived net to the cent, including the negatives; the read-only
case is unchanged.

Pre-existing rows are not backfilled, so a range spanning the deploy mixes
gross and net.

* fix: read a zero cache-write price as unpublished, not free

deepseek-chat carries a literal 0.0 cache_creation_input_token_cost. The
fall-open only caught None, so the zero was taken at face value and the
premium became 0 - input_cost -- reporting a fabricated saving of
writes * input_cost on traffic that cached nothing.

No provider gives cache writes away, so a falsy price means the same thing
an absent one does.

* test: pin that the read leg keeps a literal zero price

The two zero prices mean opposite things and the asymmetry was unpinned.
A free cache write is unpublished pricing; a free cache read is real, and
15 models charge for input while serving reads for nothing. Copying the
write leg's falsy fall-open onto the read leg would zero out their savings.

* refactor: resolve caching rates through the established pricing helpers

Addresses Greptile's P1 and P2, and replaces hand-rolled pricing lookup with
the patterns this file and the cost calculator already own:

- Deployment pricing first: rates now resolve through _effective_model_info
  (Router.get_deployment_model_info), the same helper the autorouter driver
  uses, falling back to _model_info public rates. A deployment with negotiated
  cache rates previously priced at the public map -- a 3x error on the repro.
- Individual prices read via _get_cost_per_unit, the cost calculator's
  accessor, which also coerces string prices from config.yaml and resolves
  service-tier suffixes; the previous raw .get() handled neither.
- Pricing tests no longer monkeypatch litellm.get_model_info; each case now
  pins a real pricing-map entry with a fixture-drift assertion, and the
  deployment-rate case follows the existing Router-fixture test pattern.

Behaviour on public rates is unchanged: 101 tests pass, including the exact
same live-verified formula.

* fix(cost-optimization): computeCacheLeakage divides net savings by all cached tokens, not reads alone

prompt_caching_savings_spend is net of the cache-write premium since PR #36452.
computeCacheLeakage was still dividing by cache_read_tokens alone, which:

1. Overstates the per-token rate on traffic that writes and reads cache equally:
   a 1:1 read:write key shows rate = 0.002, not 0.001, if net savings is /bin/zsh.002

2. Flips the sign on write-heavy traffic: when writes cost more than reads save
   (common on Anthropic and Bedrock), the aggregate net can go negative, but
   dividing by reads alone would show a positive 'potential savings' for keys
   that don't cache yet — recommending they start caching when it's currently
   losing money overall

Fix: divide realizedCachingSavings by (cacheReadTokens + cacheCreationTokens),
matching the semantic that a key starting to cache pays those write premiums too.

When the rate is non-positive, price nothing (potentialSavings stays null, renders
as '—'), reusing the existing no-data fallback path. The card can't meaningfully
estimate savings from a losing rate.

Rename discountPerToken → netSavingsPerCachedToken to surface the semantics and
prevent this drift in future.

Update Usage tab and Cache Leakage card tooltips to describe net-of-premium cost.

Add tests for 1:1 read:write traffic and write-heavy negative-net traffic.
2026-08-10 18:52:03 -07:00
..
a2a_protocol chore(typing): clear 1.4k basedpyright Any errors across 21 hotspot files 2026-08-08 08:14:29 -07:00
anthropic_interface chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
assistants chore(lint): clear grandfathered over-limit lint drift and ratchet budgets down 2026-08-05 12:18:13 -07:00
batch_completion feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
batches Merge pull request #35928 from BerriAI/litellm_zero_ruff_lit_headroom 2026-08-05 09:46:05 -07:00
caching refactor(types): replace implicit and explicit Any across 11 modules 2026-08-06 03:34:06 +00:00
completion_extras chore(lint): clear grandfathered over-limit lint drift and ratchet budgets down 2026-08-05 12:18:13 -07:00
compression chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
containers chore(lint): clear grandfathered over-limit lint drift and ratchet budgets down 2026-08-05 12:18:13 -07:00
endpoints/speech/speech_to_completion_bridge feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
evals chore(typing): remove 914 basedpyright Any errors across 16 hotspot files 2026-08-10 01:24:40 -07:00
experimental_mcp_client chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
files chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
fine_tuning Merge branch 'litellm_internal_staging' into litellm_remove_types_ruff_exclusion 2026-08-05 11:05:33 -07:00
google_genai Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_daily_any_cleanup_08_04_2026 2026-08-05 12:13:11 -07:00
images chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
integrations fix(arize): trace MCP tool calls instead of crashing on CallToolResult (#36453) 2026-08-10 16:37:12 -07:00
interactions chore(lint): clear grandfathered over-limit lint drift and ratchet budgets down 2026-08-05 12:18:13 -07:00
litellm_core_utils Merge pull request #36403 from BerriAI/litellm_model_registry_deprecation_audit 2026-08-10 11:26:41 -07:00
llms fix(bedrock): reject Anthropic server-side web_search tool with actionable error (#36473) 2026-08-10 15:59:18 -07:00
models chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
ocr feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
passthrough chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
proxy fix: net prompt-caching savings against the cache-write premium (#36452) 2026-08-10 18:52:03 -07:00
proxy_auth feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
rag chore(typing): remove 914 basedpyright Any errors across 16 hotspot files 2026-08-10 01:24:40 -07:00
realtime_api chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
repositories fix(reset_budget_job): atomic budget cascade with chunked reset scans (#36287) 2026-08-10 14:42:36 -07:00
rerank_api chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
responses chore(typing): restore one-line ToolParam comment 2026-08-10 07:17:06 -07:00
router_strategy feat(router): independent, default-on deployment affinity for the auto-router (#36146) 2026-08-08 13:02:29 -07:00
router_utils fix(router): warn when a deployment's credentials contradict its provider (#36486) 2026-08-10 18:41:19 -07:00
rust_bridge feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
sandbox feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
search chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
secret_managers chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
skills chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
types fix(websearch): stop leaking interception control fields to providers (#36480) 2026-08-10 17:22:37 -07:00
vector_store_files chore(lint): clear grandfathered over-limit lint drift and ratchet budgets down 2026-08-05 12:18:13 -07:00
vector_stores chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
videos chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
__init__.py feat(logging): add opt-in session_id and trace_id correlation to JSON log records via contextvars (#34418) 2026-08-10 10:40:13 -07:00
_internal_context.py feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
_lazy_imports.py fix(lint): bring basedpyright rule counts back under their budget limits 2026-08-05 10:23:02 -07:00
_lazy_imports_registry.py chore(lint): clear grandfathered over-limit lint drift and ratchet budgets down 2026-08-05 12:18:13 -07:00
_logging.py feat(logging): add opt-in session_id and trace_id correlation to JSON log records via contextvars (#34418) 2026-08-10 10:40:13 -07:00
_redis.py Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_daily_any_cleanup_08_04_2026 2026-08-05 12:13:11 -07:00
_redis_credential_provider.py chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
_service_logger.py chore(lint): clear grandfathered over-limit lint drift and ratchet budgets down 2026-08-05 12:18:13 -07:00
_uuid.py chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
_version.py
anthropic_beta_headers_config.json fix(bedrock-invoke): retain clear_tool_uses_20250919 context_management edits and emit context-management-2025-06-27 beta (LIT-3393) (#32658) 2026-07-09 14:31:27 -07:00
anthropic_beta_headers_manager.py feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
blog_posts.json docs(blog): add WebRTC blog post link 2026-03-13 17:54:33 +05:30
budget_manager.py feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
constants.py fix(reset_budget_job): atomic budget cascade with chunked reset scans (#36287) 2026-08-10 14:42:36 -07:00
cost.json
cost_calculator.py Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_zero_ruff_lit_headroom 2026-08-05 02:53:06 -07:00
exceptions.py chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
main.py fix(router): keep custom model_info across a price data reload (#35491) 2026-08-05 19:56:50 +00:00
model_prices_and_context_window_backup.json Merge pull request #36403 from BerriAI/litellm_model_registry_deprecation_audit 2026-08-10 11:26:41 -07:00
policy_templates_backup.json feat(add-new-block_code_execution-guardrail): prevent agent from executing code (#22154) 2026-02-25 22:02:14 -08:00
provider_endpoints_support_backup.json chore: litellm oss staging (#30968) 2026-06-23 07:31:44 -07:00
py.typed
router.py fix(router): warn when a deployment's credentials contradict its provider (#36486) 2026-08-10 18:41:19 -07:00
scheduler.py feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
setup_wizard.py chore(lint): strip inert type: ignore comments and zero LIT009, LIT010, LIT011 headroom 2026-08-05 02:37:24 -07:00
timeout.py feat(lint): enforce Final on locals and freeze function parameters (LIT010, LIT011) 2026-08-04 12:54:39 -07:00
utils.py feat(logging): add opt-in session_id and trace_id correlation to JSON log records via contextvars (#34418) 2026-08-10 10:40:13 -07:00