From bfef2e1e5687868084aadb69a7267d66e3a1343d Mon Sep 17 00:00:00 2001 From: Kerry Lu Date: Sat, 31 Oct 2026 09:52:39 -0700 Subject: [PATCH] fix(messages): price a native /v1/messages body as served unless its provider reports its own cost (#49356) * fix(messages): price a native /v1/messages body as served unless its provider reports its own cost * fix(messages): bill the requested speed when a native body as served reports none --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/litellm_core_utils/core_helpers.py | 7 +- litellm/litellm_core_utils/litellm_logging.py | 23 ++++++- .../test_litellm_logging.py | 65 +++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index 00151107213..fd6b6bb4a63 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -1074,9 +1074,14 @@ def billed_usage_cost(response: object, custom_llm_provider: str | None) -> floa _MESSAGES_REPORTED_COST_PROVIDERS: Final = frozenset({"opencode_go"}) +def reports_native_messages_cost(custom_llm_provider: object) -> bool: + """Whether a Messages upstream is one whose own reported cost litellm bills from instead of the price map.""" + return isinstance(custom_llm_provider, str) and custom_llm_provider in _MESSAGES_REPORTED_COST_PROVIDERS + + def native_messages_reported_cost(usage: object, custom_llm_provider: object) -> float | None: """Only a Messages upstream known to price its own responses is trusted, every other one keeps the price map.""" - if not isinstance(custom_llm_provider, str) or custom_llm_provider not in _MESSAGES_REPORTED_COST_PROVIDERS: + if not isinstance(custom_llm_provider, str) or not reports_native_messages_cost(custom_llm_provider): return None return provider_reported_usage_cost(usage, custom_llm_provider) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 3d4c29c5712..b6574a7787d 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -86,6 +86,7 @@ from litellm.litellm_core_utils.core_helpers import ( proxy_stamped_kubernetes_pod_routing, proxy_stamped_used_client_oauth_token, reconstruct_model_name, + reports_native_messages_cost, set_response_cost_in_hidden_params, single_log_line, ) @@ -5188,16 +5189,34 @@ class Logging(LiteLLMLoggingBaseClass): def billable_result(self, result: object) -> object: """The response the cost calculator prices: a /v1/messages body served through the chat - bridge is replaced by the ModelResponse its spend row is built from, anything else is + bridge is replaced by the ModelResponse its spend row is built from, a native one is only + converted when its provider reports the cost the bill is read from, anything else is priced as is.""" served: Final[object] = result call_type: Final[object] = getattr(self, "call_type", None) if not isinstance(result, dict): return served - if self.bridged_model_response is None and call_type != CallTypes.anthropic_messages.value: + if self.bridged_model_response is not None: + return self._anthropic_messages_logged_response(result=served) + if call_type != CallTypes.anthropic_messages.value: return served + if not reports_native_messages_cost(self.model_call_details.get("custom_llm_provider")): + return self._served_with_requested_speed(result=served) return self._anthropic_messages_logged_response(result=served) + def _served_with_requested_speed(self, result: object) -> object: + """A native /v1/messages body priced as served bills the speed the client asked for when its + usage block reports none, the same speed its spend row is priced at.""" + body: Final = _SERVED_ANTHROPIC_BODY.validate_python(result) + usage: Final = body.get("usage") + requested: Final = _LITELLM_PARAMS.validate_python(getattr(self, "optional_params", None) or {}).get("speed") + if not isinstance(requested, str) or not isinstance(usage, dict): + return result + reported: Final = _SERVED_ANTHROPIC_BODY.validate_python(usage) + if isinstance(reported.get("speed"), str): + return result + return {**body, "usage": {**reported, "speed": requested}} + def _anthropic_messages_logged_response(self, result: object) -> ModelResponse: """ The ModelResponse a /v1/messages spend_logs row is built from. diff --git a/tests/unit/litellm_core_utils/test_litellm_logging.py b/tests/unit/litellm_core_utils/test_litellm_logging.py index e952127a32c..93a0b0735da 100644 --- a/tests/unit/litellm_core_utils/test_litellm_logging.py +++ b/tests/unit/litellm_core_utils/test_litellm_logging.py @@ -14231,6 +14231,71 @@ def test_native_messages_body_carries_the_reported_list_cost_into_the_priced_res assert headers.get("llm_provider-x-litellm-response-cost") == expected_header +_NATIVE_PRICED_MESSAGE: Final = { + "id": "msg_native", + "type": "message", + "role": "assistant", + "model": "claude-haiku-4-5", + "content": [{"type": "text", "text": "answer"}], + "stop_reason": "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 1200, "output_tokens": 300}, +} + + +def _native_anthropic_messages_logging_obj() -> LitellmLogging: + logging_obj: Final = _anthropic_messages_logging_obj() + logging_obj.stream = False + logging_obj.optional_params = {} + logging_obj.model = "claude-haiku-4-5" + logging_obj.model_call_details["custom_llm_provider"] = "anthropic" + return logging_obj + + +@pytest.mark.parametrize( + "served_body", + ( + {**_NATIVE_PRICED_MESSAGE, "content": None}, + {key: value for key, value in _NATIVE_PRICED_MESSAGE.items() if key != "content"}, + ), + ids=("content-null", "content-missing"), +) +def test_a_native_messages_body_without_content_is_priced_from_its_usage(served_body: dict[str, object]) -> None: + """Regression: #48254 converted every native /v1/messages body before pricing it, and the conversion + raised on a 200 whose ``content`` was null or missing, turning the served 200 into a 500.""" + with_content: Final = _native_anthropic_messages_logging_obj().response_cost_calculator( + result=_NATIVE_PRICED_MESSAGE + ) + assert with_content is not None and with_content > 0 + + without_content: Final = _native_anthropic_messages_logging_obj().response_cost_calculator(result=served_body) + + assert without_content == with_content + + +def _fast_mode_native_logging_obj(requested_speed: str | None) -> LitellmLogging: + logging_obj: Final = _native_anthropic_messages_logging_obj() + logging_obj.model = "claude-opus-5" + logging_obj.model_call_details["model"] = "claude-opus-5" + logging_obj.optional_params = {"speed": requested_speed} if requested_speed else {} + return logging_obj + + +def test_a_native_messages_body_without_a_reported_speed_bills_the_requested_speed() -> None: + """A fast-mode request whose upstream usage block omits ``speed`` is still billed at the fast rate, + the same rate the spend row applies from the request's ``speed``.""" + body: Final = {**_NATIVE_PRICED_MESSAGE, "model": "claude-opus-5"} + standard: Final = _fast_mode_native_logging_obj(None).response_cost_calculator(result=body) + reported_fast: Final = _fast_mode_native_logging_obj(None).response_cost_calculator( + result={**body, "usage": {**_NATIVE_PRICED_MESSAGE["usage"], "speed": "fast"}} + ) + assert standard is not None and reported_fast is not None and reported_fast > standard + + requested_fast: Final = _fast_mode_native_logging_obj("fast").response_cost_calculator(result=body) + + assert requested_fast == reported_fast + + def test_record_native_bedrock_guardrail_bills_when_the_metadata_is_none() -> None: import datetime