From 6195f5344d9eb2cdaa71af287d7292b819e3386b Mon Sep 17 00:00:00 2001 From: lengkejun Date: Tue, 26 May 2026 15:36:12 +0800 Subject: [PATCH] fix(pricing): add cache_read_input_token_cost to 8 deepseek models 8 deepseek and openrouter/deepseek models declared only the legacy `input_cost_per_token_cache_hit` field, but the cost calculator (`litellm.litellm_core_utils.llm_cost_calc.utils._get_token_base_cost`) only reads `cache_read_input_token_cost`. As a result, prompt-cache-hit tokens were billed at $0 instead of the cache-read rate for: - deepseek/deepseek-coder - deepseek/deepseek-r1 - deepseek/deepseek-v3.2 - openrouter/deepseek/deepseek-chat-v3.1 - openrouter/deepseek/deepseek-v3.2 - openrouter/deepseek/deepseek-v3.2-exp - openrouter/deepseek/deepseek-r1 - openrouter/deepseek/deepseek-r1-0528 This patch adds `cache_read_input_token_cost` with the same value as the existing legacy field to both pricing JSONs, matching the convention already used by `deepseek/deepseek-chat`, `deepseek/deepseek-reasoner`, `deepseek/deepseek-v3`, and others. Adds a regression test that parametrizes over the 8 models and an end-to-end check that a cache hit on `deepseek/deepseek-r1` is billed at the cache-read rate via `generic_cost_per_token`. --- ...odel_prices_and_context_window_backup.json | 8 ++ model_prices_and_context_window.json | 8 ++ .../test_deepseek_cache_read_pricing.py | 110 ++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 tests/test_litellm/test_deepseek_cache_read_pricing.py diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 2cef600ea32..91550f499fb 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -15333,6 +15333,7 @@ "supports_tool_choice": true }, "deepseek/deepseek-coder": { + "cache_read_input_token_cost": 1.4e-08, "input_cost_per_token": 1.4e-07, "input_cost_per_token_cache_hit": 1.4e-08, "litellm_provider": "deepseek", @@ -15347,6 +15348,7 @@ "supports_tool_choice": true }, "deepseek/deepseek-r1": { + "cache_read_input_token_cost": 1.4e-07, "input_cost_per_token": 5.5e-07, "input_cost_per_token_cache_hit": 1.4e-07, "litellm_provider": "deepseek", @@ -15402,6 +15404,7 @@ "supports_tool_choice": true }, "deepseek/deepseek-v3.2": { + "cache_read_input_token_cost": 2.8e-08, "input_cost_per_token": 2.8e-07, "input_cost_per_token_cache_hit": 2.8e-08, "litellm_provider": "deepseek", @@ -30615,6 +30618,7 @@ "supports_tool_choice": true }, "openrouter/deepseek/deepseek-chat-v3.1": { + "cache_read_input_token_cost": 2e-08, "input_cost_per_token": 2e-07, "input_cost_per_token_cache_hit": 2e-08, "litellm_provider": "openrouter", @@ -30630,6 +30634,7 @@ "supports_tool_choice": true }, "openrouter/deepseek/deepseek-v3.2": { + "cache_read_input_token_cost": 2.8e-08, "input_cost_per_token": 2.8e-07, "input_cost_per_token_cache_hit": 2.8e-08, "litellm_provider": "openrouter", @@ -30645,6 +30650,7 @@ "supports_tool_choice": true }, "openrouter/deepseek/deepseek-v3.2-exp": { + "cache_read_input_token_cost": 2e-08, "input_cost_per_token": 2e-07, "input_cost_per_token_cache_hit": 2e-08, "litellm_provider": "openrouter", @@ -30660,6 +30666,7 @@ "supports_tool_choice": true }, "openrouter/deepseek/deepseek-r1": { + "cache_read_input_token_cost": 1.4e-07, "input_cost_per_token": 5.5e-07, "input_cost_per_token_cache_hit": 1.4e-07, "litellm_provider": "openrouter", @@ -30675,6 +30682,7 @@ "supports_tool_choice": true }, "openrouter/deepseek/deepseek-r1-0528": { + "cache_read_input_token_cost": 1.4e-07, "input_cost_per_token": 5e-07, "input_cost_per_token_cache_hit": 1.4e-07, "litellm_provider": "openrouter", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index db28118d52b..40f5558f92a 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -15333,6 +15333,7 @@ "supports_tool_choice": true }, "deepseek/deepseek-coder": { + "cache_read_input_token_cost": 1.4e-08, "input_cost_per_token": 1.4e-07, "input_cost_per_token_cache_hit": 1.4e-08, "litellm_provider": "deepseek", @@ -15347,6 +15348,7 @@ "supports_tool_choice": true }, "deepseek/deepseek-r1": { + "cache_read_input_token_cost": 1.4e-07, "input_cost_per_token": 5.5e-07, "input_cost_per_token_cache_hit": 1.4e-07, "litellm_provider": "deepseek", @@ -15402,6 +15404,7 @@ "supports_tool_choice": true }, "deepseek/deepseek-v3.2": { + "cache_read_input_token_cost": 2.8e-08, "input_cost_per_token": 2.8e-07, "input_cost_per_token_cache_hit": 2.8e-08, "litellm_provider": "deepseek", @@ -30690,6 +30693,7 @@ "supports_tool_choice": true }, "openrouter/deepseek/deepseek-chat-v3.1": { + "cache_read_input_token_cost": 2e-08, "input_cost_per_token": 2e-07, "input_cost_per_token_cache_hit": 2e-08, "litellm_provider": "openrouter", @@ -30705,6 +30709,7 @@ "supports_tool_choice": true }, "openrouter/deepseek/deepseek-v3.2": { + "cache_read_input_token_cost": 2.8e-08, "input_cost_per_token": 2.8e-07, "input_cost_per_token_cache_hit": 2.8e-08, "litellm_provider": "openrouter", @@ -30720,6 +30725,7 @@ "supports_tool_choice": true }, "openrouter/deepseek/deepseek-v3.2-exp": { + "cache_read_input_token_cost": 2e-08, "input_cost_per_token": 2e-07, "input_cost_per_token_cache_hit": 2e-08, "litellm_provider": "openrouter", @@ -30735,6 +30741,7 @@ "supports_tool_choice": true }, "openrouter/deepseek/deepseek-r1": { + "cache_read_input_token_cost": 1.4e-07, "input_cost_per_token": 5.5e-07, "input_cost_per_token_cache_hit": 1.4e-07, "litellm_provider": "openrouter", @@ -30750,6 +30757,7 @@ "supports_tool_choice": true }, "openrouter/deepseek/deepseek-r1-0528": { + "cache_read_input_token_cost": 1.4e-07, "input_cost_per_token": 5e-07, "input_cost_per_token_cache_hit": 1.4e-07, "litellm_provider": "openrouter", diff --git a/tests/test_litellm/test_deepseek_cache_read_pricing.py b/tests/test_litellm/test_deepseek_cache_read_pricing.py new file mode 100644 index 00000000000..007b163d1aa --- /dev/null +++ b/tests/test_litellm/test_deepseek_cache_read_pricing.py @@ -0,0 +1,110 @@ +""" +Regression test for DeepSeek and OpenRouter/DeepSeek models that historically +only declared ``input_cost_per_token_cache_hit`` in the pricing JSON. + +The cost calculator (``litellm.litellm_core_utils.llm_cost_calc.utils``) only +reads ``cache_read_input_token_cost`` for prompt-cache-hit pricing. When a +model defines ``input_cost_per_token_cache_hit`` without the canonical key, +cache-hit tokens are billed at $0 because the cache-read cost resolves to +``None`` -> ``0.0``. + +This test ensures the two keys are kept in sync for the affected models and +that a representative model actually bills cache-hit tokens at the cache rate. +""" + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.abspath("../..")) + +import litellm +from litellm.litellm_core_utils.llm_cost_calc.utils import generic_cost_per_token +from litellm.types.utils import PromptTokensDetailsWrapper, Usage + +# Models that previously only carried ``input_cost_per_token_cache_hit``. +# Each must also carry ``cache_read_input_token_cost`` with an identical value. +DEEPSEEK_CACHE_HIT_MODELS = [ + "deepseek/deepseek-coder", + "deepseek/deepseek-r1", + "deepseek/deepseek-v3.2", + "openrouter/deepseek/deepseek-chat-v3.1", + "openrouter/deepseek/deepseek-v3.2", + "openrouter/deepseek/deepseek-v3.2-exp", + "openrouter/deepseek/deepseek-r1", + "openrouter/deepseek/deepseek-r1-0528", +] + + +@pytest.fixture(autouse=True) +def _use_local_model_cost_map(monkeypatch): + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) + litellm.get_model_info.cache_clear() + yield + litellm.get_model_info.cache_clear() + + +@pytest.mark.parametrize("model", DEEPSEEK_CACHE_HIT_MODELS) +def test_cache_read_input_token_cost_present(model): + """``cache_read_input_token_cost`` must be present and equal to the legacy + ``input_cost_per_token_cache_hit`` field.""" + info = litellm.model_cost[model] + legacy = info.get("input_cost_per_token_cache_hit") + canonical = info.get("cache_read_input_token_cost") + assert legacy is not None, f"{model} is missing input_cost_per_token_cache_hit" + assert canonical is not None, ( + f"{model} is missing cache_read_input_token_cost; cache-hit tokens would " + f"be billed at $0 because the cost calculator only reads the canonical key." + ) + assert canonical == legacy, ( + f"{model} cache_read_input_token_cost ({canonical}) must match input_cost_per_token_cache_hit ({legacy})" + ) + + +def test_deepseek_r1_cache_hit_billed_at_cache_rate(): + """End-to-end check that a cache hit on deepseek/deepseek-r1 is billed at + the cache-read rate instead of the regular input rate.""" + model = "deepseek/deepseek-r1" + info = litellm.model_cost[model] + + prompt_tokens = 1000 + cached_tokens = 800 + text_tokens = prompt_tokens - cached_tokens + completion_tokens = 100 + + usage = Usage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=prompt_tokens + completion_tokens, + prompt_tokens_details=PromptTokensDetailsWrapper( + cached_tokens=cached_tokens, + text_tokens=text_tokens, + ), + ) + + input_cost, output_cost = generic_cost_per_token( + model=model, + usage=usage, + custom_llm_provider="deepseek", + ) + + expected_input_cost = ( + info["input_cost_per_token"] * text_tokens + info["cache_read_input_token_cost"] * cached_tokens + ) + expected_output_cost = info["output_cost_per_token"] * completion_tokens + + assert abs(input_cost - expected_input_cost) < 1e-12, ( + f"input cost mismatch: got {input_cost}, expected {expected_input_cost}" + ) + assert abs(output_cost - expected_output_cost) < 1e-12, ( + f"output cost mismatch: got {output_cost}, expected {expected_output_cost}" + ) + + # Sanity check: regression scenario (cache_read_input_token_cost missing) + # would have billed cached tokens at the full input rate. + naive_full_input_cost = info["input_cost_per_token"] * prompt_tokens + assert input_cost < naive_full_input_cost, ( + "cache-hit tokens were not discounted; cache_read_input_token_cost is likely missing from this model entry." + )