From dc58c35bba52a259f99b2d867b4936eed043d43d Mon Sep 17 00:00:00 2001 From: shivam Date: Mon, 27 Jul 2026 23:43:21 +0000 Subject: [PATCH] fix(anthropic cost): apply regional geo uplift to cached tokens Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/anthropic/cost_calculation.py | 32 ++++--- tests/test_litellm/test_cost_calculator.py | 99 ++++++++++++++++++++++ 2 files changed, 117 insertions(+), 14 deletions(-) diff --git a/litellm/llms/anthropic/cost_calculation.py b/litellm/llms/anthropic/cost_calculation.py index 6a4de1c41b4..6d0a7f8000a 100644 --- a/litellm/llms/anthropic/cost_calculation.py +++ b/litellm/llms/anthropic/cost_calculation.py @@ -24,9 +24,10 @@ def _compute_cache_only_cost(model_info: "ModelInfo", usage: "Usage", service_ti """ Return only the cache-related portion of the prompt cost (cache read + cache write). - These costs must NOT be scaled by geo/speed multipliers because the old + These costs must NOT be scaled by the ``fast`` speed multiplier because the old explicit ``fast/`` model entries carried unchanged cache rates while - multiplying only the regular input/output token costs. + multiplying only the regular input/output token costs. Regional pricing, by + contrast, uplifts every token type, so the geo multiplier does scale them. """ if usage.prompt_tokens_details is None: return 0.0 @@ -81,20 +82,23 @@ def cost_per_token(model: str, usage: "Usage", service_tier: str | None = None) model_info: Final = litellm.get_model_info(model=model, custom_llm_provider="anthropic") provider_specific_entry: Final[dict] = model_info.get("provider_specific_entry") or {} - multiplier = 1.0 - if ( - hasattr(usage, "inference_geo") - and usage.inference_geo - and usage.inference_geo.lower() not in ["global", "not_available"] - ): - multiplier *= provider_specific_entry.get(usage.inference_geo.lower(), 1.0) - if hasattr(usage, "speed") and usage.speed == "fast": - multiplier *= provider_specific_entry.get("fast", 1.0) + geo_multiplier: Final = ( + provider_specific_entry.get(usage.inference_geo.lower(), 1.0) + if getattr(usage, "inference_geo", None) and usage.inference_geo.lower() not in ("global", "not_available") + else 1.0 + ) + speed_multiplier: Final = ( + provider_specific_entry.get("fast", 1.0) if getattr(usage, "speed", None) == "fast" else 1.0 + ) - if multiplier != 1.0: + if speed_multiplier != 1.0: cache_cost: Final = _compute_cache_only_cost(model_info=model_info, usage=usage, service_tier=service_tier) - prompt_cost = (prompt_cost - cache_cost) * multiplier + cache_cost - completion_cost *= multiplier + prompt_cost = (prompt_cost - cache_cost) * speed_multiplier + cache_cost + completion_cost *= speed_multiplier + + if geo_multiplier != 1.0: + prompt_cost *= geo_multiplier + completion_cost *= geo_multiplier except Exception: pass diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index 3f024e2fd03..16f69773151 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -2726,6 +2726,105 @@ def test_anthropic_cost_per_token_prices_cache_at_served_tier_with_multiplier(): assert completion_cost == pytest.approx(expected_completion) +def _register_anthropic_geo_cache_model(model: str) -> None: + litellm.register_model( + model_cost={ + model: { + "input_cost_per_token": 5e-6, + "output_cost_per_token": 25e-6, + "cache_creation_input_token_cost": 6.25e-6, + "cache_read_input_token_cost": 0.5e-6, + "litellm_provider": "anthropic", + "max_tokens": 8192, + "provider_specific_entry": {"us": 1.1, "fast": 2.0}, + } + } + ) + + +def test_anthropic_geo_multiplier_applies_to_cache_tokens(): + """ + Regression: the regional (geo) uplift must scale cache read and cache write + cost too, not just non-cache input and output. + + Anthropic's regional surcharge applies to every token type, so a cache-heavy + row (nearly all cache-creation tokens) must still come in 10% above the + global-priced row. Before the fix the uplift was applied only to the + non-cache portion, so cache-heavy spend was under-reported by ~10%. + """ + from litellm.llms.anthropic.cost_calculation import ( + cost_per_token as anthropic_cost_per_token, + ) + from litellm.types.utils import PromptTokensDetailsWrapper, Usage + + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + model = "claude-test-geo-cache-model" + _register_anthropic_geo_cache_model(model) + + def make_usage() -> "Usage": + return Usage( + prompt_tokens=1_000_000, + completion_tokens=500, + total_tokens=1_000_500, + prompt_tokens_details=PromptTokensDetailsWrapper( + cached_tokens=200_000, + cache_creation_tokens=799_800, + ), + ) + + base_usage = make_usage() + base_prompt_cost, base_completion_cost = anthropic_cost_per_token(model=model, usage=base_usage) + + geo_usage = make_usage() + geo_usage.inference_geo = "us" + geo_prompt_cost, geo_completion_cost = anthropic_cost_per_token(model=model, usage=geo_usage) + + expected_base_prompt = 200 * 5e-6 + 200_000 * 0.5e-6 + 799_800 * 6.25e-6 + assert base_prompt_cost == pytest.approx(expected_base_prompt) + assert geo_prompt_cost == pytest.approx(expected_base_prompt * 1.1) + assert geo_completion_cost == pytest.approx(base_completion_cost * 1.1) + + +def test_anthropic_geo_and_fast_multipliers_compose(): + """ + The ``fast`` speed multiplier stays cache-exclusive (the old explicit + ``fast/`` entries kept base cache rates) while the geo multiplier scales the + whole cost, so a fast + regional row prices as + ``((non_cache * fast) + cache) * geo``. + """ + from litellm.llms.anthropic.cost_calculation import ( + cost_per_token as anthropic_cost_per_token, + ) + from litellm.types.utils import PromptTokensDetailsWrapper, Usage + + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + model = "claude-test-geo-fast-cache-model" + _register_anthropic_geo_cache_model(model) + + usage = Usage( + prompt_tokens=10_000, + completion_tokens=500, + total_tokens=10_500, + prompt_tokens_details=PromptTokensDetailsWrapper( + cached_tokens=2_000, + cache_creation_tokens=6_000, + ), + ) + usage.inference_geo = "us" + usage.speed = "fast" + + prompt_cost, completion_cost = anthropic_cost_per_token(model=model, usage=usage) + + cache_cost = 2_000 * 0.5e-6 + 6_000 * 6.25e-6 + non_cache_cost = 2_000 * 5e-6 + assert prompt_cost == pytest.approx((non_cache_cost * 2.0 + cache_cost) * 1.1) + assert completion_cost == pytest.approx(500 * 25e-6 * 2.0 * 1.1) + + def test_gemini_cache_tokens_details_no_negative_values(): """ Test for Issue #18750: Negative text_tokens with Gemini caching