diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 6d1448a7e2c..749f724aaf0 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1883,7 +1883,7 @@ class Logging(LiteLLMLoggingBaseClass): router_model_id = self.get_router_model_id() ## RESPONSE COST ## - custom_pricing: Final = self._custom_pricing_for(priced_result) + custom_pricing: Final = self.custom_pricing_for(priced_result) prompt = self._prompt_for_cost_calculation() @@ -2021,7 +2021,7 @@ class Logging(LiteLLMLoggingBaseClass): model=model, completion_response=result, custom_llm_provider=custom_llm_provider, - custom_pricing=self._custom_pricing_for(result), + custom_pricing=self.custom_pricing_for(result), base_model=_get_base_model_from_metadata(model_call_details=self.model_call_details), router_model_id=router_model_id or self.get_router_model_id(), region_name=_resolve_mantle_region_for_cost( @@ -2046,7 +2046,7 @@ class Logging(LiteLLMLoggingBaseClass): usage=usage, ) - def _custom_pricing_for(self, result: object) -> bool: + def custom_pricing_for(self, result: object) -> bool: litellm_params: Final = getattr(self, "litellm_params", None) result_hidden_params: Final = getattr(result, "_hidden_params", None) or MappingProxyType({}) additional_headers: Final = ( @@ -6612,7 +6612,7 @@ def get_standard_logging_object_payload( # Fall back to the actual deployment model set by the router in metadata. if base_model is None: base_model = metadata.get("deployment") - custom_pricing: Final = use_custom_pricing_for_model(litellm_params=litellm_params) + custom_pricing: Final = logging_obj.custom_pricing_for(init_response_obj) raw_response_cost: Final = kwargs.get("response_cost") llm_response_cost: Final[float] = raw_response_cost or 0.0 guardrail_cost: Final = guardrail_information_cost(metadata.get("standard_logging_guardrail_information")) diff --git a/tests/unit/litellm_core_utils/test_litellm_logging.py b/tests/unit/litellm_core_utils/test_litellm_logging.py index d6ca8d1fa67..2c83bb9bf2a 100644 --- a/tests/unit/litellm_core_utils/test_litellm_logging.py +++ b/tests/unit/litellm_core_utils/test_litellm_logging.py @@ -28,6 +28,7 @@ from litellm.litellm_core_utils.litellm_logging import Logging as LitellmLogging from litellm.litellm_core_utils.litellm_logging import ( _extract_response_obj_and_hidden_params, _get_status_fields, + get_standard_logging_object_payload, set_callbacks, ) from litellm.llms.base_llm.ocr.transformation import OCRUsageInfo @@ -8685,6 +8686,85 @@ class TestAzurePTUSpilloverCost: finally: self._unregister_models() + def test_spillover_payload_reports_the_pay_as_you_go_entry_billing_used(self, monkeypatch) -> None: + self._register_models() + try: + obj = self._logging_obj(dict(self.PTU_MODEL_INFO), flag="True", litellm_rate=0.0, monkeypatch=monkeypatch) + response = self._response() + response._hidden_params["additional_headers"] = {"llm_provider-x-ms-is-spilled-over": "true"} + response_cost: Final = obj._response_cost_calculator(result=response) + + payload: Final = get_standard_logging_object_payload( + kwargs={ + "model": self.SERVED_MODEL, + "custom_llm_provider": "azure", + "litellm_params": obj.litellm_params, + "response_cost": response_cost, + }, + init_response_obj=response, + start_time=datetime.datetime.now(), + end_time=datetime.datetime.now(), + logging_obj=obj, + status="success", + ) + + assert response_cost == pytest.approx(self.EXPECTED_SPILL_COST) + assert payload is not None + model_map_value: Final = payload["model_map_information"]["model_map_value"] + assert model_map_value is not None + assert model_map_value["input_cost_per_token"] == litellm.model_cost[self.SERVED_MODEL]["input_cost_per_token"] + assert model_map_value["output_cost_per_token"] == litellm.model_cost[self.SERVED_MODEL]["output_cost_per_token"] + assert response_cost == pytest.approx( + 100 * model_map_value["input_cost_per_token"] + 50 * model_map_value["output_cost_per_token"] + ) + finally: + self._unregister_models() + +||||||| parent of c6ae60c2eb (fixup! fix(logging): report the pay-as-you-go entry billing used on Azure PTU spillover (#47567)) + def test_spillover_without_base_model_reports_the_pay_as_you_go_rates_billing_used(self, monkeypatch) -> None: + self._register_models() + litellm.model_cost[self.PTU_DEPLOYMENT] = {"litellm_provider": "azure", "mode": "chat"} + try: + obj = self._logging_obj( + dict(self.PTU_MODEL_INFO), + flag="True", + litellm_rate=0.0, + monkeypatch=monkeypatch, + deployment=self.PTU_DEPLOYMENT, + ) + response = self._response() + response._hidden_params["additional_headers"] = {"llm_provider-x-ms-is-spilled-over": "true"} + response._hidden_params["provider_response_model"] = response.model + response_cost: Final = obj._response_cost_calculator(result=response) + response.model = "spill-model-group-alias" + + payload: Final = get_standard_logging_object_payload( + kwargs={ + "model": self.SERVED_MODEL, + "custom_llm_provider": "azure", + "litellm_params": obj.litellm_params, + "response_cost": response_cost, + }, + init_response_obj=response, + start_time=datetime.datetime.now(), + end_time=datetime.datetime.now(), + logging_obj=obj, + status="success", + ) + + assert response_cost == pytest.approx(self.EXPECTED_SPILL_COST) + assert payload is not None + model_map_information: Final = payload["model_map_information"] + assert model_map_information["model_map_key"] != self.PTU_DEPLOYMENT + model_map_value: Final = model_map_information["model_map_value"] + assert model_map_value is not None + assert response_cost == pytest.approx( + 100 * model_map_value["input_cost_per_token"] + 50 * model_map_value["output_cost_per_token"] + ) + finally: + litellm.model_cost.pop(self.PTU_DEPLOYMENT, None) + self._unregister_models() + def test_spillover_header_does_not_touch_non_ptu_custom_pricing(self, monkeypatch) -> None: self._register_models() custom_model_id: Final = "non-ptu-custom-router-model-id"