mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
fix(cost_calculator): apply discount and margin to image and video costs
This commit is contained in:
parent
24123269cc
commit
c235ab295c
2 changed files with 196 additions and 40 deletions
|
|
@ -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
|
||||
):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue