From c235ab295c324f29a0ff9fdd884ff0c91b000216 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:42:00 +0000 Subject: [PATCH] fix(cost_calculator): apply discount and margin to image and video costs --- litellm/cost_calculator.py | 119 ++++++++++++++------- tests/test_litellm/test_cost_calculator.py | 117 ++++++++++++++++++++ 2 files changed, 196 insertions(+), 40 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 96aed20529f..c0c109e407c 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -1105,6 +1105,57 @@ def _store_cost_breakdown_in_logging_obj( pass +def _apply_cost_adjustments( + base_cost: float, + custom_llm_provider: str | None, + litellm_logging_obj: LitellmLoggingObject | None, + input_cost: float, + output_cost: float, +) -> float: + """ + Apply configured discount + margin to a provider cost and record the breakdown. + + Args: + base_cost: Provider cost before discount/margin + custom_llm_provider: The LLM provider name + litellm_logging_obj: Logging object the cost breakdown is stored on + input_cost: Portion of base_cost attributable to input + output_cost: Portion of base_cost attributable to output + + Returns: + The cost after discount and margin + """ + discounted_cost, discount_percent, discount_amount = _apply_cost_discount( + base_cost=base_cost, + custom_llm_provider=custom_llm_provider, + ) + ( + final_cost, + margin_percent, + margin_fixed_amount, + margin_total_amount, + ) = _apply_cost_margin( + base_cost=discounted_cost, + custom_llm_provider=custom_llm_provider, + ) + + _store_cost_breakdown_in_logging_obj( + litellm_logging_obj=litellm_logging_obj, + prompt_tokens_cost_usd_dollar=input_cost, + completion_tokens_cost_usd_dollar=output_cost, + cost_for_built_in_tools_cost_usd_dollar=0.0, + total_cost_usd_dollar=final_cost, + original_cost=base_cost, + discount_percent=discount_percent, + discount_amount=discount_amount, + margin_percent=margin_percent, + margin_fixed_amount=margin_fixed_amount, + margin_total_amount=margin_total_amount, + ) + + return final_cost + + def completion_cost( completion_response=None, model: Optional[str] = None, @@ -1335,7 +1386,7 @@ def completion_cost( completion_response, ImageResponse ): ### IMAGE GENERATION COST CALCULATION ### - return CostCalculatorUtils.route_image_generation_cost_calculator( + _image_cost = CostCalculatorUtils.route_image_generation_cost_calculator( model=model, custom_llm_provider=custom_llm_provider, completion_response=completion_response, @@ -1345,6 +1396,13 @@ def completion_cost( optional_params=optional_params, call_type=call_type, ) + return _apply_cost_adjustments( + base_cost=_image_cost, + custom_llm_provider=custom_llm_provider, + litellm_logging_obj=litellm_logging_obj, + input_cost=0.0, + output_cost=_image_cost, + ) elif call_type in _VIDEO_CALL_TYPES: ### VIDEO GENERATION COST CALCULATION ### # Extract custom model_info for deployment-specific pricing @@ -1375,21 +1433,35 @@ def completion_cost( video_generation_cost, ) - return video_generation_cost( + _video_cost = video_generation_cost( model=model, duration_seconds=duration_seconds, custom_llm_provider=custom_llm_provider, model_info=_video_model_info, video_resolution=video_resolution, ) + return _apply_cost_adjustments( + base_cost=_video_cost, + custom_llm_provider=custom_llm_provider, + litellm_logging_obj=litellm_logging_obj, + input_cost=0.0, + output_cost=_video_cost, + ) # Fallback to default video cost calculation if no duration available - return default_video_cost_calculator( + _video_cost = default_video_cost_calculator( model=model, duration_seconds=0.0, # Default to 0 if no duration available custom_llm_provider=custom_llm_provider, model_info=_video_model_info, video_resolution=video_resolution, ) + return _apply_cost_adjustments( + base_cost=_video_cost, + custom_llm_provider=custom_llm_provider, + litellm_logging_obj=litellm_logging_obj, + input_cost=0.0, + output_cost=_video_cost, + ) elif call_type in _SPEECH_CALL_TYPES: prompt_characters = litellm.utils._count_characters(text=prompt) elif call_type in _TRANSCRIPTION_CALL_TYPES: @@ -1446,46 +1518,13 @@ def completion_cost( ) # Return the total cost (prompt_cost + completion_cost, but for search it's just prompt_cost) - _final_cost = prompt_cost + completion_cost_result - - # Apply discount - original_cost = _final_cost - ( - _final_cost, - discount_percent, - discount_amount, - ) = _apply_cost_discount( - base_cost=_final_cost, + return _apply_cost_adjustments( + base_cost=prompt_cost + completion_cost_result, custom_llm_provider=custom_llm_provider, - ) - - # Apply margin from module-level config if configured - ( - _final_cost, - margin_percent, - margin_fixed_amount, - margin_total_amount, - ) = _apply_cost_margin( - base_cost=_final_cost, - custom_llm_provider=custom_llm_provider, - ) - - # Store cost breakdown in logging object if available - _store_cost_breakdown_in_logging_obj( litellm_logging_obj=litellm_logging_obj, - prompt_tokens_cost_usd_dollar=prompt_cost, - completion_tokens_cost_usd_dollar=completion_cost_result, - cost_for_built_in_tools_cost_usd_dollar=0.0, - total_cost_usd_dollar=_final_cost, - original_cost=original_cost, - discount_percent=discount_percent, - discount_amount=discount_amount, - margin_percent=margin_percent, - margin_fixed_amount=margin_fixed_amount, - margin_total_amount=margin_total_amount, + input_cost=prompt_cost, + output_cost=completion_cost_result, ) - - return _final_cost elif call_type == _AREALTIME_CALL_TYPE and isinstance( completion_response, LiteLLMRealtimeStreamLoggingObject ): diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index 276ee96ed65..b998cb745d8 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -3509,3 +3509,120 @@ def test_combine_usage_objects_sums_mirrored_cache_write_fields_once(): assert combined_pair.prompt_tokens_details is not None assert combined_pair.prompt_tokens_details.cache_write_tokens == 100 assert combined_pair.prompt_tokens_details.cache_creation_tokens == 100 + +def test_image_generation_cost_applies_discount_and_margin(monkeypatch): + """ + Regression for https://github.com/BerriAI/litellm/issues/34731: image generation + returned the raw provider cost, bypassing cost_discount_config / cost_margin_config. + """ + from litellm.types.utils import ImageObject, ImageResponse + + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) + + response = ImageResponse( + created=1234567890, + data=[ImageObject(url="https://example.com/image.png")], + quality="standard", + size="1024x1024", + ) + cost_kwargs = dict( + completion_response=response, + model="dall-e-3", + custom_llm_provider="openai", + call_type="image_generation", + size="1024-x-1024", + ) + + monkeypatch.setattr(litellm, "cost_discount_config", {}) + monkeypatch.setattr(litellm, "cost_margin_config", {}) + base_cost = completion_cost(**cost_kwargs) + assert base_cost > 0 + + monkeypatch.setattr(litellm, "cost_discount_config", {"openai": 0.05}) + monkeypatch.setattr(litellm, "cost_margin_config", {"openai": 0.10}) + adjusted_cost = completion_cost(**cost_kwargs) + + assert adjusted_cost == pytest.approx(base_cost * 0.95 * 1.10, rel=1e-9) + + +def test_video_generation_cost_applies_discount_and_margin(monkeypatch): + """ + Regression for https://github.com/BerriAI/litellm/issues/34731: video generation + returned the raw provider cost, bypassing cost_discount_config / cost_margin_config. + """ + from unittest.mock import MagicMock + + response = MagicMock() + response.usage = MagicMock() + response.usage.duration_seconds = 10.0 + response.usage.video_resolution = None + type(response)._hidden_params = {} + + logging_obj = MagicMock() + logging_obj.litellm_params = {"metadata": {"model_info": {"output_cost_per_video_per_second": 0.05}}} + + cost_kwargs = dict( + completion_response=response, + model="openai/hunyuanvideo", + call_type="create_video", + custom_llm_provider="openai", + custom_pricing=True, + litellm_logging_obj=logging_obj, + ) + + monkeypatch.setattr(litellm, "cost_discount_config", {}) + monkeypatch.setattr(litellm, "cost_margin_config", {}) + assert completion_cost(**cost_kwargs) == pytest.approx(0.5) + + monkeypatch.setattr(litellm, "cost_discount_config", {"openai": 0.05}) + monkeypatch.setattr(litellm, "cost_margin_config", {"openai": {"percentage": 0.10, "fixed_amount": 0.01}}) + adjusted_cost = completion_cost(**cost_kwargs) + + discounted = 0.5 * 0.95 + assert adjusted_cost == pytest.approx(discounted + discounted * 0.10 + 0.01, rel=1e-9) + + +def test_video_generation_cost_records_discount_breakdown(monkeypatch): + """ + The discount/margin breakdown must reach the logging object for media endpoints too, + so spend logs can show original vs charged cost. + """ + from unittest.mock import MagicMock + + from litellm.litellm_core_utils.litellm_logging import Logging + + response = MagicMock() + response.usage = MagicMock() + response.usage.duration_seconds = 10.0 + response.usage.video_resolution = None + type(response)._hidden_params = {} + + logging_obj = Logging( + model="openai/hunyuanvideo", + messages=[], + stream=False, + call_type="create_video", + start_time=None, + litellm_call_id="test-call-id", + function_id="test-function-id", + ) + logging_obj.litellm_params = {"metadata": {"model_info": {"output_cost_per_video_per_second": 0.05}}} + + monkeypatch.setattr(litellm, "cost_discount_config", {"openai": 0.05}) + monkeypatch.setattr(litellm, "cost_margin_config", {}) + + cost = completion_cost( + completion_response=response, + model="openai/hunyuanvideo", + call_type="create_video", + custom_llm_provider="openai", + custom_pricing=True, + litellm_logging_obj=logging_obj, + ) + + assert cost == pytest.approx(0.475) + assert logging_obj.cost_breakdown is not None + assert logging_obj.cost_breakdown["original_cost"] == pytest.approx(0.5) + assert logging_obj.cost_breakdown["discount_amount"] == pytest.approx(0.025) + assert logging_obj.cost_breakdown["total_cost"] == pytest.approx(0.475)