diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index abc22713be4..f20b66790c4 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -3503,7 +3503,9 @@ class Logging(LiteLLMLoggingBaseClass): else: return None - def _handle_anthropic_messages_response_logging(self, result: Any) -> ModelResponse: + def _handle_anthropic_messages_response_logging( + self, result: Any + ) -> Union[ModelResponse, ResponsesAPIResponse]: """ Handles logging for Anthropic messages responses. @@ -3522,6 +3524,15 @@ class Logging(LiteLLMLoggingBaseClass): return result elif isinstance(result, ModelResponse): return result + elif isinstance( + result, + (ResponseCompletedEvent, ResponseIncompleteEvent, ResponseFailedEvent), + ): + # anthropic_messages() can route to OpenAI Responses API; in that path + # the assembled streaming result is one of these terminal events rather than + # a ModelResponse. Return the inner response so downstream handlers + # (_transform_usage_objects, normalize_logging_result) can process it. + return result.response httpx_response = self.model_call_details.get("httpx_response", None) if httpx_response and isinstance(httpx_response, httpx.Response): diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index 8acda74a156..d57d8dafdbd 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -3114,3 +3114,44 @@ def test_get_error_information_prefers_message_attribute_over_empty_str(): ) assert info["error_message"] == "real failure detail" assert info["error_code"] == "401" + + +@pytest.mark.parametrize( + "event_cls, event_type", + [ + ("ResponseCompletedEvent", "response.completed"), + ("ResponseIncompleteEvent", "response.incomplete"), + ("ResponseFailedEvent", "response.failed"), + ], +) +def test_handle_anthropic_messages_response_logging_with_terminal_responses_api_events( + event_cls, event_type +): + """Regression test for #28943: when anthropic_messages routes to OpenAI Responses + API and stream=True, success_handler receives a terminal ResponsesAPI event instead + of a ModelResponse. The handler must return the inner ResponsesAPIResponse rather + than crashing with AnthropicResponse.model_validate.""" + import importlib + + openai_types = importlib.import_module("litellm.types.llms.openai") + EventClass = getattr(openai_types, event_cls) + from litellm.types.llms.openai import ResponsesAPIResponse + + logging_obj = LitellmLogging( + model="gpt-4o", + messages=[{"role": "user", "content": "hello"}], + stream=True, + call_type="anthropic_messages", + start_time=time.time(), + litellm_call_id="test-rce-123", + function_id="test-fn", + ) + + inner_response = ResponsesAPIResponse( + id="resp_test", created_at=1700000000, output=[] + ) + event = EventClass(type=event_type, response=inner_response) + + result = logging_obj._handle_anthropic_messages_response_logging(result=event) + + assert result is inner_response