diff --git a/litellm/litellm_core_utils/llm_response_utils/response_metadata.py b/litellm/litellm_core_utils/llm_response_utils/response_metadata.py index 216f2b6a45c..ef47e2f0d9f 100644 --- a/litellm/litellm_core_utils/llm_response_utils/response_metadata.py +++ b/litellm/litellm_core_utils/llm_response_utils/response_metadata.py @@ -9,7 +9,11 @@ from pydantic import TypeAdapter, ValidationError import litellm from litellm.constants import LITELLM_DETAILED_TIMING from litellm.litellm_core_utils.core_helpers import get_litellm_metadata_from_kwargs, process_response_headers -from litellm.litellm_core_utils.hidden_params import stamp_billable_usage_cost +from litellm.litellm_core_utils.hidden_params import ( + USAGE_COST_BEFORE_LITELLM_STAMP, + get_hidden_params, + stamp_billable_usage_cost, +) from litellm.litellm_core_utils.llm_response_utils.get_api_base import get_api_base from litellm.litellm_core_utils.logging_utils import LiteLLMLoggingObject from litellm.types.llms.openai import ResponseAPIUsage, ResponsesAPIResponse @@ -197,7 +201,12 @@ class ResponseMetadata: return if getattr(usage, "cost", None) is None and not litellm.include_cost_in_streaming_usage: return - stamp_billable_usage_cost(self.result, usage, response_cost) + if stamp_billable_usage_cost(self.result, usage, response_cost) is None: + return + stamped: Final = get_hidden_params(self.result) + if stamped is None or USAGE_COST_BEFORE_LITELLM_STAMP not in stamped: + return + self._update_hidden_params({USAGE_COST_BEFORE_LITELLM_STAMP: stamped[USAGE_COST_BEFORE_LITELLM_STAMP]}) def _update_hidden_params(self, new_params: Mapping[str, object]) -> None: """ diff --git a/tests/unit/test_cost_calculator.py b/tests/unit/test_cost_calculator.py index ad4e0749ce9..b6ff6cadb8d 100644 --- a/tests/unit/test_cost_calculator.py +++ b/tests/unit/test_cost_calculator.py @@ -10846,6 +10846,34 @@ def test_include_cost_in_streaming_usage_puts_the_bill_on_a_plain_reply_without_ assert logging_obj.response_cost_calculator(result=response, ignore_stamped_cost=True) == pytest.approx(bill) +def test_a_bill_stamped_on_a_plain_reply_survives_the_metadata_pass_for_a_second_pricing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The metadata pass installs a fresh hidden-params dict on a reply that arrived with none, which dropped the + provider's figure the stamp keeps and let a second pricing pass apply the margin twice.""" + monkeypatch.setattr(litellm, "cost_margin_config", {"global": {"percentage": 0.5}}) + monkeypatch.delitem(litellm.model_cost, _REPORTED_COST_FIXTURE_MODEL, raising=False) + logging_obj: Final = _reported_cost_logging(False, {}) + response: Final = ModelResponse( + id="chatcmpl-reported", + model="reported-cost-fixture", + choices=[Choices(index=0, message=Message(role="assistant", content="hi"), finish_reason="stop")], + usage=_reported_cost_usage(), + ) + + update_response_metadata( + result=response, + logging_obj=logging_obj, + model=_REPORTED_COST_FIXTURE_MODEL, + kwargs={}, + start_time=datetime.datetime(2026, 1, 1), + end_time=datetime.datetime(2026, 1, 1, 0, 0, 1), + ) + + assert response.usage is not None and response.usage.cost == pytest.approx(0.037 * 1.5) + assert logging_obj.response_cost_calculator(result=response, ignore_stamped_cost=True) == pytest.approx(0.037 * 1.5) + + def test_provider_reported_usage_cost_splits_input_and_output_by_the_token_math_ratio( monkeypatch: pytest.MonkeyPatch, ) -> None: