From d94f2612f33475d064b0fd0b6fb60ebc7881f3d0 Mon Sep 17 00:00:00 2001 From: its-amann Date: Fri, 12 Jun 2026 18:29:24 +0530 Subject: [PATCH] Include multimodal tokens in tiered pricing threshold --- .../litellm_core_utils/llm_cost_calc/utils.py | 30 +++++++--- tests/test_litellm/test_cost_calculator.py | 57 +++++++++++++++++++ 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 13da1bb648c..4035a31d425 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -212,6 +212,7 @@ def _get_effective_prompt_tokens_for_tiered_pricing(usage: Usage) -> float: them separate. When prompt_tokens_details is present, prefer the category fields so the threshold check can avoid double-counting cache tokens. """ + prompt_tokens = float(getattr(usage, "prompt_tokens", 0) or 0) if usage.prompt_tokens_details is not None: details = usage.prompt_tokens_details raw_text_tokens = getattr(details, "text_tokens", None) @@ -222,17 +223,29 @@ def _get_effective_prompt_tokens_for_tiered_pricing(usage: Usage) -> float: # If prompt_tokens already covers the cache detail total, use it as # the rolled-up input total. Otherwise cache tokens were reported # outside prompt_tokens and must be added for the tier threshold. - prompt_tokens = float(getattr(usage, "prompt_tokens", 0) or 0) cache_detail_tokens = cached_tokens + cache_creation if cache_detail_tokens > 0 and prompt_tokens < cache_detail_tokens: return prompt_tokens + cache_detail_tokens return prompt_tokens text_tokens = float(raw_text_tokens or 0) - return text_tokens + cached_tokens + cache_creation + audio_tokens = float(getattr(details, "audio_tokens", 0) or 0) + image_tokens = float(getattr(details, "image_tokens", 0) or 0) + video_tokens = float(getattr(details, "video_tokens", 0) or 0) + detail_total = ( + text_tokens + + audio_tokens + + image_tokens + + video_tokens + + cached_tokens + + cache_creation + ) + cache_detail_tokens = cached_tokens + cache_creation + if cache_detail_tokens > 0 and detail_total > prompt_tokens: + return prompt_tokens + return max(prompt_tokens, detail_total) # No prompt_tokens_details. Add explicit cache fields only if they are # not already rolled into prompt_tokens (determined by their presence). - prompt_tokens = float(getattr(usage, "prompt_tokens", 0) or 0) cache_read_tokens = float(getattr(usage, "cache_read_input_tokens", 0) or 0) cache_creation_tokens = float(getattr(usage, "cache_creation_input_tokens", 0) or 0) return prompt_tokens + cache_read_tokens + cache_creation_tokens @@ -674,10 +687,13 @@ def _calculate_input_cost( # First check if input_cost_per_image_token is available. If not, default to generic input_cost_per_token. image_token_cost_key = "input_cost_per_image_token" if model_info.get(image_token_cost_key) is None: - image_token_cost_key = "input_cost_per_token" - prompt_cost += calculate_cost_component( - model_info, image_token_cost_key, prompt_tokens_details["image_tokens"] - ) + prompt_cost += ( + float(prompt_tokens_details["image_tokens"]) * prompt_base_cost + ) + else: + prompt_cost += calculate_cost_component( + model_info, image_token_cost_key, prompt_tokens_details["image_tokens"] + ) ### CACHE WRITING COST - Now uses tiered pricing if ( diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index 50c0834e502..5a987a1c5e4 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -3038,6 +3038,63 @@ def test_custom_pricing_does_not_double_count_cached_tokens_when_text_tokens_mis litellm.model_cost.pop(f"anthropic/{model_id}", None) +def test_custom_pricing_uses_tiered_rate_when_multimodal_details_cross_threshold(): + """ + Multimodal prompt detail tokens should count toward tier thresholds even + when text_tokens is present. + """ + + model_id = "tiered-custom-pricing-multimodal-details-test" + model_info = { + "litellm_provider": "openai", + "mode": "chat", + "input_cost_per_token": 0.000005, + "output_cost_per_token": 0.000025, + "input_cost_per_token_above_200k_tokens": 0.00001, + "output_cost_per_token_above_200k_tokens": 0.0000375, + } + text_tokens = 150_000 + image_tokens = 60_001 + + litellm.register_model(model_cost={model_id: model_info}) + try: + response = ModelResponse( + id="test-id", + created=1234567890, + model=f"openai/{model_id}", + object="chat.completion", + choices=[], + usage=Usage( + prompt_tokens=text_tokens + image_tokens, + completion_tokens=0, + total_tokens=text_tokens + image_tokens, + prompt_tokens_details=PromptTokensDetailsWrapper( + text_tokens=text_tokens, + image_tokens=image_tokens, + ), + ), + ) + + cost = completion_cost( + completion_response=response, + model=f"openai/{model_id}", + custom_llm_provider="openai", + custom_pricing=True, + router_model_id=model_id, + ) + + expected_cost = ( + text_tokens + image_tokens + ) * model_info["input_cost_per_token_above_200k_tokens"] + base_cost = (text_tokens + image_tokens) * model_info["input_cost_per_token"] + + assert cost == pytest.approx(expected_cost) + assert cost != pytest.approx(base_cost) + finally: + litellm.model_cost.pop(model_id, None) + litellm.model_cost.pop(f"openai/{model_id}", None) + + def test_extract_cache_read_tokens_anthropic_top_level(): from litellm.proxy.db.db_spend_update_writer import _extract_cache_read_tokens