mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-01 07:03:02 +00:00
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>
This commit is contained in:
parent
7b4bdde96e
commit
bfef2e1e56
3 changed files with 92 additions and 3 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue