From e94a97fcfc5e793fc1de5f4291a782d4ba36dc98 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:44:22 -0700 Subject: [PATCH] fix(cost_calculator): mirror the anthropic geo uplift in the token-type cost breakdown --- .../litellm_core_utils/llm_cost_calc/utils.py | 25 +++++++ litellm/llms/anthropic/cost_calculation.py | 7 +- .../llm_cost_calc/test_llm_cost_calc_utils.py | 69 +++++++++++++++++++ 3 files changed, 96 insertions(+), 5 deletions(-) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index b94851794f0..38bdf89981e 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -694,6 +694,23 @@ def _get_regional_uplift_multiplier(model_info: ModelInfo, data_residency: str | return 1.0 +def get_provider_specific_geo_multiplier(model_info: ModelInfo, usage: Usage) -> float: + """ + Resolve the provider-specific regional pricing multiplier for the geo the + request was served from (``usage.inference_geo``), e.g. Anthropic's ``us: 1.1`` + stored under ``provider_specific_entry``. The regional surcharge applies to + every token type, so per-type cost breakdowns must scale by it too. + + Returns 1.0 when the request was served globally or the model carries no + multiplier for the geo. + """ + inference_geo: Final = getattr(usage, "inference_geo", None) + if not isinstance(inference_geo, str) or inference_geo.lower() in ("global", "not_available"): + return 1.0 + provider_specific_entry: Final[dict[str, float]] = model_info.get("provider_specific_entry") or {} + return float(provider_specific_entry.get(inference_geo.lower(), 1.0)) + + def _resolve_reasoning_token_cost( model_info: ModelInfo, service_tier: str | None, @@ -981,6 +998,14 @@ def get_token_type_cost_breakdown( cache_read_cost *= uplift cache_creation_cost *= uplift + # Mirror the provider-specific geo uplift (e.g. Anthropic us: 1.1) the totals + # apply, so cache and reasoning line items stay reconciled with them. + geo_multiplier: Final = get_provider_specific_geo_multiplier(model_info=model_info, usage=usage) + if geo_multiplier != 1.0: + reasoning_cost *= geo_multiplier + cache_read_cost *= geo_multiplier + cache_creation_cost *= geo_multiplier + return TokenTypeCostBreakdown( reasoning_cost=reasoning_cost, cache_read_cost=cache_read_cost, diff --git a/litellm/llms/anthropic/cost_calculation.py b/litellm/llms/anthropic/cost_calculation.py index 6d0a7f8000a..e792f69622c 100644 --- a/litellm/llms/anthropic/cost_calculation.py +++ b/litellm/llms/anthropic/cost_calculation.py @@ -13,6 +13,7 @@ from litellm.litellm_core_utils.llm_cost_calc.utils import ( _parse_prompt_tokens_details, calculate_cache_writing_cost, generic_cost_per_token, + get_provider_specific_geo_multiplier, ) if TYPE_CHECKING: @@ -82,11 +83,7 @@ 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 {} - 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 - ) + geo_multiplier: Final = get_provider_specific_geo_multiplier(model_info=model_info, usage=usage) speed_multiplier: Final = ( provider_specific_entry.get("fast", 1.0) if getattr(usage, "speed", None) == "fast" else 1.0 ) 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 3aa41e18f1e..d22a139ba79 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 @@ -2558,6 +2558,75 @@ def test_token_type_cost_breakdown_applies_regional_uplift(): assert text_input_cost + eu.cache_read_cost == pytest.approx(prompt_cost) +def test_token_type_cost_breakdown_applies_anthropic_geo_multiplier(monkeypatch): + """ + Anthropic's regional (geo) uplift lives in provider_specific_entry and is + applied to every token type in the totals, so the per-type breakdown must + scale its cache and reasoning line items by it too. Otherwise the logged + cache costs stay at the base rate and the cache uplift is misattributed to + plain input for exactly the cache-heavy regional traffic the uplift targets. + """ + from litellm.llms.anthropic.cost_calculation import ( + cost_per_token as anthropic_cost_per_token, + ) + + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + litellm.model_cost = litellm.get_model_cost_map(url="") + + model = "claude-test-geo-breakdown-model" + 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}, + } + } + ) + + def make_usage() -> Usage: + return 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, + ), + completion_tokens_details=CompletionTokensDetailsWrapper( + reasoning_tokens=200, text_tokens=300 + ), + ) + + base_usage = make_usage() + geo_usage = make_usage() + geo_usage.inference_geo = "us" + + base = get_token_type_cost_breakdown( + model=model, custom_llm_provider="anthropic", usage=base_usage + ) + geo = get_token_type_cost_breakdown( + model=model, custom_llm_provider="anthropic", usage=geo_usage + ) + + assert base.cache_read_cost == pytest.approx(2_000 * 0.5e-6) + assert base.cache_creation_cost == pytest.approx(6_000 * 6.25e-6) + assert geo.cache_read_cost == pytest.approx(base.cache_read_cost * 1.1) + assert geo.cache_creation_cost == pytest.approx(base.cache_creation_cost * 1.1) + assert geo.reasoning_cost == pytest.approx(base.reasoning_cost * 1.1) + + # The uplifted breakdown must still reconcile with the uplifted totals. + prompt_cost, completion_cost = anthropic_cost_per_token(model=model, usage=geo_usage) + text_input_cost = 2_000 * 5e-6 * 1.1 + text_output_cost = 300 * 25e-6 * 1.1 + assert text_input_cost + geo.cache_read_cost + geo.cache_creation_cost == pytest.approx(prompt_cost) + assert text_output_cost + geo.reasoning_cost == pytest.approx(completion_cost) + + @pytest.mark.parametrize("details_as_dict", [True, False]) def test_image_response_input_image_tokens_priced_at_image_rate(details_as_dict): """