diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 0a52e1d283e..fdc6127814f 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -310,6 +310,14 @@ def _get_token_base_cost( _get_cost_per_unit(model_info, "cache_creation_input_token_cost_above_1hr"), ) cache_read_cost = cast(float, _get_cost_per_unit(model_info, cache_read_cost_key)) + # Some model entries only carry the legacy `input_cost_per_token_cache_hit` field + # instead of the canonical `cache_read_input_token_cost` (e.g. DeepSeek entries + # added before the field was standardized). Without this fallback, cache-hit + # tokens silently cost $0 for those models. See BerriAI/litellm#28854. + if model_info.get("cache_read_input_token_cost") is None: + legacy_cache_hit_cost: Final = model_info.get("input_cost_per_token_cache_hit") + if legacy_cache_hit_cost is not None: + cache_read_cost = cast(float, _get_cost_per_unit(model_info, "input_cost_per_token_cache_hit")) ## CHECK IF ABOVE THRESHOLD # Optimization: collect threshold keys first to avoid sorting all model_info keys. diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py index 6f513ce1bd4..e9781e9f615 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py @@ -408,6 +408,74 @@ def test_get_token_base_cost_picks_highest_crossed_tier(): assert prompt_base_cost == 9e-6 +def test_get_token_base_cost_falls_back_to_legacy_cache_hit_field(): + """Regression test for #28854. + + Model entries added before `cache_read_input_token_cost` was standardized (several + DeepSeek entries) only declare the legacy `input_cost_per_token_cache_hit` field. + Without a fallback, cache-hit tokens for those models are silently billed at $0 + instead of the documented discounted rate. + """ + model_info = { + "input_cost_per_token": 1.4e-07, + "output_cost_per_token": 2.8e-07, + "input_cost_per_token_cache_hit": 1.4e-08, + } + usage = Usage(prompt_tokens=1000, completion_tokens=10, total_tokens=1010) + + cache_read_cost = _get_token_base_cost(model_info, usage)[4] + + assert cache_read_cost == 1.4e-08 + + +def test_get_token_base_cost_prefers_canonical_over_legacy_cache_hit_field(): + """The canonical `cache_read_input_token_cost` always wins when both fields are + present, so already-migrated model entries are unaffected by the fallback.""" + model_info = { + "input_cost_per_token": 1.4e-07, + "output_cost_per_token": 2.8e-07, + "input_cost_per_token_cache_hit": 1.4e-08, + "cache_read_input_token_cost": 7e-08, + } + usage = Usage(prompt_tokens=1000, completion_tokens=10, total_tokens=1010) + + cache_read_cost = _get_token_base_cost(model_info, usage)[4] + + assert cache_read_cost == 7e-08 + + +def test_generic_cost_per_token_bills_cache_hits_via_legacy_field(): + """End-to-end regression test for #28854: a model that only declares the legacy + `input_cost_per_token_cache_hit` field must bill cached tokens at that rate + instead of silently charging $0.""" + model_info = { + "input_cost_per_token": 1.4e-07, + "output_cost_per_token": 2.8e-07, + "input_cost_per_token_cache_hit": 1.4e-08, + } + prompt_tokens = 1000 + cached_tokens = 400 + usage = Usage( + prompt_tokens=prompt_tokens, + completion_tokens=10, + total_tokens=prompt_tokens + 10, + prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=cached_tokens), + ) + + prompt_cost, _ = generic_cost_per_token( + model="fake-legacy-cache-hit-model", + usage=usage, + custom_llm_provider="deepseek", + model_info=model_info, + ) + + text_tokens = prompt_tokens - cached_tokens + expected_prompt_cost = ( + text_tokens * model_info["input_cost_per_token"] + cached_tokens * model_info["input_cost_per_token_cache_hit"] + ) + assert round(prompt_cost, 12) == round(expected_prompt_cost, 12) + + def test_generic_cost_per_token_gpt54_above_272k_tokens(_local_model_cost_map): """GPT-5.4/5.4-pro: prompts >272K input tokens priced at 2x input, 1.5x output.""" model = "gpt-5.4"