mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
fix(cost): price the /v1/messages response cost header from the converted message body
This commit is contained in:
parent
d024c2d980
commit
4cefdf4f72
2 changed files with 84 additions and 16 deletions
|
|
@ -1656,7 +1656,11 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
):
|
||||
return 0.0
|
||||
|
||||
transformed_result: Final = self._generate_content_result_as_model_response(result)
|
||||
transformed_result: Final = (
|
||||
self._anthropic_message_result_as_model_response(result)
|
||||
if self.call_type == CallTypes.anthropic_messages.value
|
||||
else self._generate_content_result_as_model_response(result)
|
||||
)
|
||||
if transformed_result is not None:
|
||||
result = transformed_result
|
||||
|
||||
|
|
@ -3878,23 +3882,32 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
litellm_params={},
|
||||
)
|
||||
else:
|
||||
from litellm.types.llms.anthropic import AnthropicResponse
|
||||
|
||||
pydantic_result: Final = AnthropicResponse.model_validate(result)
|
||||
import httpx
|
||||
|
||||
result = litellm.AnthropicConfig().transform_parsed_response(
|
||||
completion_response=pydantic_result.model_dump(),
|
||||
raw_response=httpx.Response(
|
||||
status_code=200,
|
||||
headers={},
|
||||
),
|
||||
model_response=litellm.ModelResponse(id=provider_response_id),
|
||||
json_mode=None,
|
||||
speed=self.optional_params.get("speed") if self.optional_params else None,
|
||||
)
|
||||
result = self._anthropic_message_body_as_model_response(result)
|
||||
return result
|
||||
|
||||
def _anthropic_message_result_as_model_response(self, result: object) -> ModelResponse | None:
|
||||
if not isinstance(result, dict) or result.get("type") != "message":
|
||||
return None
|
||||
try:
|
||||
return self._anthropic_message_body_as_model_response(result)
|
||||
except Exception as e: # noqa: BLE001 # cost normalization must never break the response path
|
||||
verbose_logger.debug("anthropic message response cost normalization failed: %s", e)
|
||||
return None
|
||||
|
||||
def _anthropic_message_body_as_model_response(self, body: object) -> ModelResponse:
|
||||
import httpx
|
||||
|
||||
from litellm.types.llms.anthropic import AnthropicResponse
|
||||
|
||||
parsed: Final = AnthropicResponse.model_validate(body)
|
||||
return litellm.AnthropicConfig().transform_parsed_response(
|
||||
completion_response=parsed.model_dump(),
|
||||
raw_response=httpx.Response(status_code=200, headers={}),
|
||||
model_response=litellm.ModelResponse(id=_provider_response_id(body)),
|
||||
json_mode=None,
|
||||
speed=self.optional_params.get("speed") if self.optional_params else None,
|
||||
)
|
||||
|
||||
def _translate_responses_api_response_to_model_response(self, result: ResponsesAPIResponse) -> ModelResponse:
|
||||
"""
|
||||
Convert a Responses API response into a ModelResponse for spend_logs.
|
||||
|
|
|
|||
|
|
@ -2013,6 +2013,61 @@ def test_response_cost_calculator_does_not_transform_non_generate_content_dict()
|
|||
assert not cost
|
||||
|
||||
|
||||
def test_response_cost_calculator_anthropic_message_body_bills_thinking_at_the_reasoning_rate(monkeypatch):
|
||||
"""
|
||||
Regression for LIT-6908: the ``x-litellm-response-cost`` header for a non-streaming
|
||||
/v1/messages call is priced synchronously from the raw Anthropic message body, whose
|
||||
usage carries no reasoning split, so thinking tokens were billed at the plain output
|
||||
rate while the spend log (priced from the converted ``ModelResponse``) billed them at
|
||||
``output_cost_per_reasoning_token``. The calculator now converts the body first, so
|
||||
both surfaces agree.
|
||||
"""
|
||||
monkeypatch.setitem(
|
||||
litellm.model_cost,
|
||||
"lit6908-thinker",
|
||||
{
|
||||
"litellm_provider": "deepseek",
|
||||
"input_cost_per_token": 1e-06,
|
||||
"output_cost_per_token": 2e-06,
|
||||
"output_cost_per_reasoning_token": 1e-05,
|
||||
},
|
||||
)
|
||||
logging_obj = LitellmLogging(
|
||||
model="lit6908-thinker",
|
||||
messages=[{"role": "user", "content": "What is 17 * 23? Answer with just the number."}],
|
||||
stream=False,
|
||||
call_type="anthropic_messages",
|
||||
start_time=time.time(),
|
||||
litellm_call_id="lit6908",
|
||||
function_id="lit6908",
|
||||
)
|
||||
logging_obj.model_call_details["custom_llm_provider"] = "deepseek"
|
||||
logging_obj.optional_params = {}
|
||||
thinking = "We need answer 17*23=391. Just number."
|
||||
body = {
|
||||
"id": "msg_lit6908",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "lit6908-thinker",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": thinking, "signature": ""},
|
||||
{"type": "text", "text": "391"},
|
||||
],
|
||||
"stop_reason": "end_turn",
|
||||
"stop_sequence": None,
|
||||
"usage": {"input_tokens": 97, "output_tokens": 15},
|
||||
}
|
||||
reasoning_tokens = litellm.token_counter(text=thinking, count_response_tokens=True)
|
||||
assert 0 < reasoning_tokens < 15
|
||||
|
||||
cost = logging_obj._response_cost_calculator(result=body)
|
||||
|
||||
assert cost == pytest.approx(97 * 1e-06 + (15 - reasoning_tokens) * 2e-06 + reasoning_tokens * 1e-05)
|
||||
assert cost == pytest.approx(
|
||||
logging_obj._response_cost_calculator(result=logging_obj._anthropic_messages_logged_response(body))
|
||||
)
|
||||
|
||||
|
||||
def _file_content_logging_obj(call_type: str) -> LitellmLogging:
|
||||
logging_obj = LitellmLogging(
|
||||
model="gemini-3-flash-preview",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue