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 4379b937394..9a548018bb4 100644 --- a/litellm/litellm_core_utils/llm_response_utils/response_metadata.py +++ b/litellm/litellm_core_utils/llm_response_utils/response_metadata.py @@ -185,11 +185,12 @@ class ResponseMetadata: self._update_hidden_params(new_params) def _stamp_billed_usage_cost(self, response_cost: object) -> None: - """A client reads what litellm billed from usage.cost, streamed or not, priced deployment or not.""" + """A usage.cost the upstream reported becomes what litellm billed, so a client and the spend log agree on a + priced deployment; a reply without one keeps its usage as the upstream sent it.""" if not isinstance(self.result, (ModelResponse, ResponsesAPIResponse)): return usage: Final = getattr(self.result, "usage", None) - if not isinstance(usage, (Usage, ResponseAPIUsage)): + if not isinstance(usage, (Usage, ResponseAPIUsage)) or getattr(usage, "cost", None) is None: return stamp_billable_usage_cost(self.result, usage, response_cost) diff --git a/tests/unit/test_cost_calculator.py b/tests/unit/test_cost_calculator.py index 83724d7a2fc..c4266ec3e75 100644 --- a/tests/unit/test_cost_calculator.py +++ b/tests/unit/test_cost_calculator.py @@ -10787,6 +10787,33 @@ def test_the_client_reads_what_litellm_billed_from_usage_cost_on_every_route( assert logging_obj.response_cost_calculator(result=response, ignore_stamped_cost=True) == pytest.approx(bill) +def test_a_plain_reply_without_an_upstream_usage_cost_keeps_none_after_pricing(monkeypatch: pytest.MonkeyPatch) -> None: + """Clients comparing the usage dict to the upstream's byte for byte must not find a cost litellm added.""" + monkeypatch.setitem( + litellm.model_cost, + _PRICED_DEPLOYMENT_ID, + {"litellm_provider": "openai", "mode": "chat", "input_cost_per_token": 2e-6, "output_cost_per_token": 6e-6}, + ) + response: Final = ModelResponse( + id="chatcmpl-unreported", + model="reported-cost-fixture", + choices=[Choices(index=0, message=Message(role="assistant", content="hi"), finish_reason="stop")], + usage=Usage(prompt_tokens=100, completion_tokens=20, total_tokens=120), + ) + + update_response_metadata( + result=response, + logging_obj=_priced_logging(False, "completion"), + 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 getattr(response.usage, "cost", None) is None + assert response._hidden_params["response_cost"] == pytest.approx(100 * 2e-6 + 20 * 6e-6) + + def test_provider_reported_usage_cost_splits_input_and_output_by_the_token_math_ratio( monkeypatch: pytest.MonkeyPatch, ) -> None: