fix(logging): preserve prompt/completion tokens in non-streaming /v1/responses usage logging

This commit is contained in:
Devin AI 2026-07-17 15:39:43 +00:00
parent 4d33964898
commit 607d068d38
2 changed files with 45 additions and 8 deletions

View file

@ -1778,16 +1778,13 @@ class Logging(LiteLLMLoggingBaseClass):
if isinstance(result, ResponsesAPIResponse):
result = result.model_copy()
transformed_usage = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(result.usage)
setattr(result, "usage", transformed_usage)
transformed_usage_dict = (
transformed_usage.model_dump() if hasattr(transformed_usage, "model_dump") else dict(transformed_usage)
)
setattr(result, "usage", transformed_usage_dict)
if (standard_logging_payload := self.model_call_details.get("standard_logging_object")) is not None:
response_dict = result.model_dump() if hasattr(result, "model_dump") else dict(result)
# Ensure usage is properly included with transformed chat format
if transformed_usage is not None:
response_dict["usage"] = (
transformed_usage.model_dump()
if hasattr(transformed_usage, "model_dump")
else dict(transformed_usage)
)
response_dict["usage"] = transformed_usage_dict
standard_logging_payload["response"] = response_dict
elif isinstance(result, TranscriptionResponse):
from litellm.litellm_core_utils.llm_cost_calc.usage_object_transformation import (

View file

@ -3639,6 +3639,46 @@ def test_handle_anthropic_messages_response_logging_degrades_on_unparseable_resp
assert result.usage.prompt_tokens == 4 # type: ignore[attr-defined]
def _responses_logging_obj():
return LitellmLogging(
model="openai/gpt-4o",
messages=[{"role": "user", "content": "hi"}],
stream=False,
call_type="aresponses",
start_time=time.time(),
litellm_call_id="33688",
function_id="33688",
)
def test_transform_usage_objects_keeps_prompt_and_completion_tokens_serializable():
"""Regression for #33688. _transform_usage_objects transforms a Responses API
usage object into the chat Usage shape, but it must store it as a plain dict on
result.usage. If it stores the Pydantic Usage object instead, result.model_dump()
serializes it under the declared ResponseAPIUsage schema and silently drops
prompt_tokens/completion_tokens (only total_tokens survives), so Prometheus,
custom callbacks, and SpendLogs lose those counts."""
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
logging_obj = _responses_logging_obj()
result = logging_obj._transform_usage_objects(result=_responses_api_response_with_text())
assert isinstance(result.usage, dict)
assert result.usage["prompt_tokens"] == 11
assert result.usage["completion_tokens"] == 7
assert result.usage["total_tokens"] == 18
dumped = result.model_dump()
assert dumped["usage"]["prompt_tokens"] == 11
assert dumped["usage"]["completion_tokens"] == 7
assert dumped["usage"]["total_tokens"] == 18
usage_dict = StandardLoggingPayloadSetup.get_usage_as_dict(response_obj=dumped)
assert usage_dict["prompt_tokens"] == 11
assert usage_dict["completion_tokens"] == 7
assert usage_dict["total_tokens"] == 18
class _SuccessCapturingLogger(CustomLogger):
"""Records the success payload. success_payload is populated only in
async_log_success_event, so it stays None when the buggy no-op