From b748ba7ca38667468417e30b9f22d8bfa7e95d79 Mon Sep 17 00:00:00 2001 From: silencedoctor <33445544+silencedoctor@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:13:02 +0800 Subject: [PATCH] feat(cost): support deployment-level cost discounts --- litellm/cost_calculator.py | 62 +++++++--- litellm/main.py | 12 +- litellm/types/router.py | 4 + litellm/types/utils.py | 9 ++ .../proxy/test_pricing_field_strip.py | 1 + tests/test_litellm/test_cost_calculator.py | 60 +++++++++ .../test_register_model_custom_pricing.py | 117 ++++++++++++++++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 6 + 8 files changed, 253 insertions(+), 18 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 8f7cd09d364..8e93ccd4cf1 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -157,6 +157,7 @@ _SPEECH_CALL_TYPES: Final = frozenset( } ) +_COST_DISCOUNT_FIELD: Final = "cost_discount" _TRANSCRIPTION_CALL_TYPES: Final = frozenset( { CallTypes.atranscription.value, @@ -960,13 +961,17 @@ def _infer_call_type(call_type: CallTypesLiteral | None, completion_response: An def _apply_cost_discount( base_cost: float, custom_llm_provider: str | None, + model: str | None = None, + model_info: ModelInfo | None = None, ) -> tuple[float, float, float]: """ - Apply provider-specific cost discount from module-level config. + Apply model/deployment-specific cost discount or provider-specific config. Args: base_cost: The base cost before discount custom_llm_provider: The LLM provider name + model: Model cost-map key used for cost calculation + model_info: Optional deployment/model pricing metadata Returns: Tuple of (final_cost, discount_percent, discount_amount) @@ -975,14 +980,21 @@ def _apply_cost_discount( discount_percent = 0.0 discount_amount = 0.0 - if custom_llm_provider and custom_llm_provider in litellm.cost_discount_config: + model_cost_discount = _get_model_cost_discount(model=model, model_info=model_info) + discount_source = custom_llm_provider + if model_cost_discount is not None: + discount_percent = model_cost_discount + discount_source = _COST_DISCOUNT_FIELD + elif custom_llm_provider and custom_llm_provider in litellm.cost_discount_config: discount_percent = litellm.cost_discount_config[custom_llm_provider] + + if discount_percent: discount_amount = original_cost * discount_percent final_cost: Final = original_cost - discount_amount if verbose_logger.isEnabledFor(logging.DEBUG): verbose_logger.debug( - f"Applied {discount_percent * 100}% discount to {custom_llm_provider}: " + f"Applied {discount_percent * 100}% discount to {discount_source}: " f"${original_cost:.6f} -> ${final_cost:.6f} (saved ${discount_amount:.6f})" ) @@ -991,6 +1003,28 @@ def _apply_cost_discount( return base_cost, discount_percent, discount_amount +def _get_model_cost_discount( + model: str | None = None, + model_info: ModelInfo | None = None, +) -> float | None: + discount: Any = None + if model_info is not None: + discount = model_info.get(_COST_DISCOUNT_FIELD) + + if discount is None and model is not None: + registered_model_info = litellm.model_cost.get(model) + if isinstance(registered_model_info, dict): + discount = registered_model_info.get(_COST_DISCOUNT_FIELD) + + if discount is None: + return None + + discount_float = float(discount) + if not 0 <= discount_float <= 1: + raise ValueError("cost_discount must be between 0 and 1") + return discount_float + + def _apply_cost_margin( base_cost: float, custom_llm_provider: str | None, @@ -1477,6 +1511,7 @@ def completion_cost( ) = _apply_cost_discount( base_cost=_final_cost, custom_llm_provider=custom_llm_provider, + model=router_model_id or model, ) # Apply margin from module-level config if configured @@ -1630,18 +1665,15 @@ def completion_cost( _final_cost += sum(additional_costs.values()) original_cost = _final_cost - if litellm.cost_discount_config: - ( - _final_cost, - discount_percent, - discount_amount, - ) = _apply_cost_discount( - base_cost=_final_cost, - custom_llm_provider=custom_llm_provider, - ) - else: - discount_percent = 0.0 - discount_amount = 0.0 + ( + _final_cost, + discount_percent, + discount_amount, + ) = _apply_cost_discount( + base_cost=_final_cost, + custom_llm_provider=custom_llm_provider, + model=router_model_id or model, + ) # Apply margin from module-level config if configured if litellm.cost_margin_config: diff --git a/litellm/main.py b/litellm/main.py index 2cf53833c5a..c40a2233272 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -5283,8 +5283,10 @@ def completion( ### REGISTER CUSTOM MODEL PRICING -- IF GIVEN ### if ( - input_cost_per_token is not None and output_cost_per_token is not None - ) or input_cost_per_second is not None: + (input_cost_per_token is not None and output_cost_per_token is not None) + or input_cost_per_second is not None + or kwargs.get("cost_discount") is not None + ): _register_custom_pricing_for_request( model=model, custom_llm_provider=custom_llm_provider, @@ -6139,7 +6141,11 @@ def embedding( ) ### REGISTER CUSTOM MODEL PRICING -- IF GIVEN ### - if (input_cost_per_token is not None and output_cost_per_token is not None) or input_cost_per_second is not None: + if ( + (input_cost_per_token is not None and output_cost_per_token is not None) + or input_cost_per_second is not None + or kwargs.get("cost_discount") is not None + ): _register_custom_pricing_for_request( model=model, custom_llm_provider=custom_llm_provider, diff --git a/litellm/types/router.py b/litellm/types/router.py index 99a4603ae49..981881a1912 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -148,6 +148,7 @@ class ModelInfo(MirroredPricingParams): base_model: str | None = None # specify if the base model is azure/gpt-3.5-turbo etc for accurate cost tracking tier: Literal["free", "paid"] | None = None + cost_discount: float | None = None """ Team Model Specific Fields @@ -208,6 +209,8 @@ class ModelInfo(MirroredPricingParams): end: Final = _as_utc(self.ptu_effective_to) if start is not None and end is not None and end <= start: raise ValueError("ptu_effective_to must be after ptu_effective_from") + if self.cost_discount is not None and not 0 <= self.cost_discount <= 1: + raise ValueError("cost_discount must be between 0 and 1") return self model_config = ConfigDict(extra="allow") @@ -482,6 +485,7 @@ class LiteLLMParamsTypedDict(TypedDict, total=False): output_cost_per_second: float | None output_cost_per_second_1080p: float | None num_retries: int | None + cost_discount: float | None ## MOCK RESPONSES ## mock_response: str | ModelResponse | Exception | None diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 67eae2b4f21..8ffa17a3099 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -219,6 +219,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False): cache_read_input_token_cost_above_272k_tokens_priority: float | None cache_read_input_token_cost_above_272k_tokens_flex: float | None cache_read_input_token_cost_above_512k_tokens: float | None + cost_discount: float | None # Smallest prefix this model will actually cache, whatever caching mechanism its provider uses. # Absent means the provider-agnostic default applies; see MINIMUM_PROMPT_CACHE_TOKEN_COUNT. prompt_cache_min_tokens: int | None @@ -3333,6 +3334,7 @@ class CustomPricingLiteLLMParams(MirroredPricingParams): output_cost_per_second_1080p: float | None = None input_cost_per_pixel: float | None = None output_cost_per_pixel: float | None = None + cost_discount: float | None = None # Include all ModelInfoBase fields as optional # This allows any model_info parameter to be set in litellm_params @@ -3411,6 +3413,13 @@ class CustomPricingLiteLLMParams(MirroredPricingParams): regional_processing_uplift_multiplier_us: float | None = None regional_endpoint_uplift_multiplier: float | None = None + @field_validator("cost_discount") + @classmethod + def validate_cost_discount(cls, value: float | None) -> float | None: + if value is not None and not 0 <= value <= 1: + raise ValueError("cost_discount must be between 0 and 1") + return value + @classmethod def strip_custom_pricing_fields(cls, model_info: dict[str, Any]) -> dict[str, Any]: """Return a copy of ``model_info`` without per-deployment custom pricing fields. diff --git a/tests/test_litellm/proxy/test_pricing_field_strip.py b/tests/test_litellm/proxy/test_pricing_field_strip.py index 1707f5bbc05..3ab3c888bc6 100644 --- a/tests/test_litellm/proxy/test_pricing_field_strip.py +++ b/tests/test_litellm/proxy/test_pricing_field_strip.py @@ -63,6 +63,7 @@ class TestStripClientPricingOverrides: ) # Sanity: the obvious top-level pricing fields are in the set. for field in ( + "cost_discount", "input_cost_per_token", "output_cost_per_token", "input_cost_per_second", diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index 8dad4bef07b..3758db2c9c4 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -1885,6 +1885,66 @@ def test_cost_discount_not_applied_to_other_providers(monkeypatch): print(f" - Cost remains unchanged: ${cost_with_selective_discount:.6f}") +def test_deployment_cost_discount_from_router_model_id_overrides_provider_discount(): + """ + Test that deployment-level discounts can be configured without overriding + the base model prices, and take precedence over provider-level discounts. + """ + original_discount_config = litellm.cost_discount_config.copy() + deployment_model_id = "test-deployment-cost-discount" + original_deployment_entry = litellm.model_cost.get(deployment_model_id) + + def _response() -> ModelResponse: + return ModelResponse( + id="test-id", + choices=[], + created=1234567890, + model="gpt-4o-mini", + object="chat.completion", + usage=Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150), + ) + + try: + litellm.cost_discount_config = {} + base_cost = completion_cost( + completion_response=_response(), + model="gpt-4o-mini", + custom_llm_provider="openai", + ) + + litellm.model_cost[deployment_model_id] = {"cost_discount": 0.8} + litellm.cost_discount_config = {"openai": 0.05} + + discounted_cost = completion_cost( + completion_response=_response(), + model="gpt-4o-mini", + custom_llm_provider="openai", + custom_pricing=True, + router_model_id=deployment_model_id, + ) + + assert discounted_cost == pytest.approx(base_cost * 0.2, rel=1e-9) + finally: + litellm.cost_discount_config = original_discount_config + if original_deployment_entry is None: + litellm.model_cost.pop(deployment_model_id, None) + else: + litellm.model_cost[deployment_model_id] = original_deployment_entry + + +def test_cost_discount_validation(): + from litellm.types.router import ModelInfo as RouterModelInfo + from litellm.types.utils import CustomPricingLiteLLMParams + + CustomPricingLiteLLMParams(cost_discount=0.8) + RouterModelInfo(cost_discount=0.8) + + with pytest.raises(ValueError, match="cost_discount must be between 0 and 1"): + CustomPricingLiteLLMParams(cost_discount=1.1) + with pytest.raises(ValueError, match="cost_discount must be between 0 and 1"): + RouterModelInfo(cost_discount=1.1) + + def test_cost_margin_percentage(monkeypatch): """ Test that percentage-based cost margin is applied correctly diff --git a/tests/test_litellm/test_register_model_custom_pricing.py b/tests/test_litellm/test_register_model_custom_pricing.py index e3f6a1a0f40..8ad069e5a54 100644 --- a/tests/test_litellm/test_register_model_custom_pricing.py +++ b/tests/test_litellm/test_register_model_custom_pricing.py @@ -17,6 +17,7 @@ import pytest import litellm from litellm.main import _build_custom_pricing_entry +from litellm.types.router import ModelInfo from litellm.utils import _invalidate_model_cost_lowercase_map @@ -37,6 +38,7 @@ def test_build_custom_pricing_entry_includes_all_kwargs_fields(): """All CustomPricingLiteLLMParams fields present in kwargs should be included in the resulting entry dict.""" kwargs = { + "cost_discount": 0.8, "input_cost_per_token": 0.001, "output_cost_per_token": 0.002, "cache_read_input_token_cost": 0.00025, @@ -52,6 +54,7 @@ def test_build_custom_pricing_entry_includes_all_kwargs_fields(): ) assert entry["litellm_provider"] == "openai" + assert entry["cost_discount"] == 0.8 assert entry["input_cost_per_token"] == 0.001 assert entry["output_cost_per_token"] == 0.002 assert entry["cache_read_input_token_cost"] == 0.00025 @@ -61,6 +64,120 @@ def test_build_custom_pricing_entry_includes_all_kwargs_fields(): assert "unrelated_kwarg" not in entry +def test_model_info_validates_cost_discount(): + ModelInfo(cost_discount=0.8) + with pytest.raises(ValueError, match="cost_discount must be between 0 and 1"): + ModelInfo(cost_discount=1.1) + + +def test_router_registers_deployment_cost_discount_and_litellm_params_override(): + model_info_only_id = "deployment-cost-discount-model-info" + litellm_params_only_id = "deployment-cost-discount-litellm-params" + litellm_params_override_id = "deployment-cost-discount-override" + shared_backend_key = "openai/gpt-4o-mini" + snapshot = _snapshot_model_cost_entries( + [ + model_info_only_id, + litellm_params_only_id, + litellm_params_override_id, + shared_backend_key, + ] + ) + + try: + litellm.Router( + model_list=[ + { + "model_name": "deployment-cost-discount-test", + "litellm_params": { + "model": shared_backend_key, + "api_key": "test-api-key", + }, + "model_info": { + "id": model_info_only_id, + "cost_discount": 0.6, + }, + }, + { + "model_name": "deployment-cost-discount-test", + "litellm_params": { + "model": shared_backend_key, + "api_key": "test-api-key", + "cost_discount": 0.7, + }, + "model_info": { + "id": litellm_params_only_id, + }, + }, + { + "model_name": "deployment-cost-discount-test", + "litellm_params": { + "model": shared_backend_key, + "api_key": "test-api-key", + "cost_discount": 0.8, + }, + "model_info": { + "id": litellm_params_override_id, + "cost_discount": 0.4, + }, + }, + ] + ) + + assert litellm.model_cost[model_info_only_id]["cost_discount"] == 0.6 + assert litellm.model_cost[litellm_params_only_id]["cost_discount"] == 0.7 + assert litellm.model_cost[litellm_params_override_id]["cost_discount"] == 0.8 + shared_backend_entry = litellm.model_cost.get(shared_backend_key) + if shared_backend_entry is not None: + assert "cost_discount" not in shared_backend_entry + finally: + _restore_model_cost_entries(snapshot) + + +def test_router_completion_applies_deployment_cost_discount_without_custom_base_prices(): + shared_backend_key = "openai/gpt-4o-mini" + deployment_id = "deployment-cost-discount-runtime" + snapshot = _snapshot_model_cost_entries([deployment_id, shared_backend_key]) + + try: + router = litellm.Router( + model_list=[ + { + "model_name": "deployment-runtime-discount-test", + "litellm_params": { + "model": shared_backend_key, + "api_key": "test-api-key", + }, + "model_info": { + "id": deployment_id, + "cost_discount": 0.8, + }, + }, + ] + ) + + messages = [{"role": "user", "content": "hello"}] + discounted_response = router.completion( + model="deployment-runtime-discount-test", + messages=messages, + mock_response="ok", + max_tokens=20, + ) + undiscounted_response = litellm.completion( + model=shared_backend_key, + messages=messages, + mock_response="ok", + max_tokens=20, + ) + + assert discounted_response._hidden_params["response_cost"] == pytest.approx( + undiscounted_response._hidden_params["response_cost"] * 0.2, + rel=1e-9, + ) + finally: + _restore_model_cost_entries(snapshot) + + def test_build_custom_pricing_entry_merges_model_info_metadata(): """Fields from model_info (mode, supports_prompt_caching, max_tokens) should be merged into the entry when present.""" diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index d311bfa3cbc..009c71fadef 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -27493,6 +27493,8 @@ export interface components { complexity_router_default_model?: string | null; /** Configurable Clientside Auth Params */ configurable_clientside_auth_params?: (string | components["schemas"]["ConfigurableClientsideParamsCustomAuth-Input"])[] | null; + /** Cost Discount */ + cost_discount?: number | null; /** Custom Llm Provider */ custom_llm_provider?: string | null; /** Default Api Key Rpm Limit */ @@ -36534,6 +36536,8 @@ export interface components { cache_read_input_token_cost?: number | null; /** Cost Per Ptu Per Hour */ cost_per_ptu_per_hour?: number | null; + /** Cost Discount */ + cost_discount?: number | null; /** Created At */ created_at?: string | null; /** Created By */ @@ -36702,6 +36706,8 @@ export interface components { complexity_router_default_model?: string | null; /** Configurable Clientside Auth Params */ configurable_clientside_auth_params?: (string | components["schemas"]["ConfigurableClientsideParamsCustomAuth-Input"])[] | null; + /** Cost Discount */ + cost_discount?: number | null; /** Custom Llm Provider */ custom_llm_provider?: string | null; /** Default Api Key Rpm Limit */