mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-28 06:23:07 +00:00
fix(logging): convert Usage object to dict in non-streaming /v1/responses transform
_transform_usage_objects was setattr-ing a raw Usage Pydantic object instead of a dict when setting result.usage for non-streaming ResponsesAPIResponse results. Since ResponsesAPIResponse.usage is typed as ResponseAPIUsage (which lacks prompt_tokens/completion_tokens), model_dump() would silently drop those fields, corrupting token usage reporting for non-streaming /v1/responses calls. The standard_logging_payload branch already converted to dict correctly; this applies the same pattern to the result.usage assignment itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
52d5a51fcb
commit
a46d18a2e0
2 changed files with 36 additions and 9 deletions
|
|
@ -2686,17 +2686,13 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
) -> object:
|
||||
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)
|
||||
chat_usage: Final = ResponseAPILoggingUtils.transform_response_api_usage_to_chat_usage(result.usage)
|
||||
chat_usage_dict: Final = chat_usage.model_dump() if hasattr(chat_usage, "model_dump") else dict(chat_usage)
|
||||
setattr(result, "usage", chat_usage_dict)
|
||||
if standard_logging_payload is not None:
|
||||
response_dict: Final = 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)
|
||||
)
|
||||
if chat_usage is not None:
|
||||
response_dict["usage"] = chat_usage_dict
|
||||
standard_logging_payload["response"] = response_dict # rebind-ok: mirrors the transformed usage
|
||||
elif isinstance(result, TranscriptionResponse):
|
||||
from litellm.litellm_core_utils.llm_cost_calc.usage_object_transformation import (
|
||||
|
|
|
|||
|
|
@ -6185,6 +6185,37 @@ def test_success_handler_computes_cost_for_dict_response():
|
|||
assert logging_obj.model_call_details["response_cost"] == expected_cost
|
||||
|
||||
|
||||
def test_transform_usage_objects_non_streaming_preserves_prompt_and_completion_tokens():
|
||||
response = ResponsesAPIResponse(
|
||||
id="resp-test-001",
|
||||
created_at=1700000000,
|
||||
output=[],
|
||||
usage=ResponseAPIUsage(input_tokens=100, output_tokens=50, total_tokens=150),
|
||||
)
|
||||
|
||||
logging_obj = LitellmLogging(
|
||||
model="gpt-5",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
stream=False,
|
||||
call_type="responses",
|
||||
start_time=time.time(),
|
||||
litellm_call_id="test-usage-preservation",
|
||||
function_id="test-fn-usage",
|
||||
)
|
||||
|
||||
result = logging_obj._transform_usage_objects(response)
|
||||
|
||||
assert isinstance(result.usage, dict)
|
||||
assert result.usage.get("prompt_tokens") == 100
|
||||
assert result.usage.get("completion_tokens") == 50
|
||||
assert result.usage.get("total_tokens") == 150
|
||||
|
||||
dumped_usage = result.model_dump().get("usage", {})
|
||||
assert dumped_usage.get("prompt_tokens") == 100
|
||||
assert dumped_usage.get("completion_tokens") == 50
|
||||
assert dumped_usage.get("total_tokens") == 150
|
||||
|
||||
|
||||
def test_success_handler_preserves_precomputed_cost_for_dict_response():
|
||||
"""Precomputed response_cost on model_call_details must not be overwritten."""
|
||||
logging_obj = _make_dict_logging_obj()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue