diff --git a/litellm/llms/openai/responses/guardrail_translation/handler.py b/litellm/llms/openai/responses/guardrail_translation/handler.py index 0fdea47415f..4a3f5acd146 100644 --- a/litellm/llms/openai/responses/guardrail_translation/handler.py +++ b/litellm/llms/openai/responses/guardrail_translation/handler.py @@ -30,7 +30,7 @@ Output: response.output is List[GenericResponseOutputItem] where each has: from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast -from openai import BaseModel +from pydantic import BaseModel from litellm._logging import verbose_proxy_logger from litellm.llms.base_llm.guardrail_translation.base_translation import BaseTranslation @@ -424,6 +424,7 @@ class OpenAIResponsesHandler(BaseTranslation): Override this method to customize text/image/tool extraction logic. """ + # Check if this is a tool call (OutputFunctionToolCall) if isinstance(output_item, OutputFunctionToolCall): if tool_calls_to_check is not None: diff --git a/litellm/utils.py b/litellm/utils.py index f5ecc07cc1d..524e86cfbbe 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -810,7 +810,12 @@ def function_setup( # noqa: PLR0915 or call_type == CallTypes.responses.value ): # Handle both 'input' (standard Responses API) and 'messages' (Cursor chat format) - messages = args[0] if len(args) > 0 else kwargs.get("input") or kwargs.get("messages", "default-message-value") + messages = ( + args[0] + if len(args) > 0 + else kwargs.get("input") + or kwargs.get("messages", "default-message-value") + ) else: messages = "default-message-value" stream = False @@ -6900,7 +6905,9 @@ def last_assistant_with_tool_calls_has_no_thinking_blocks( # Check if it has thinking_blocks thinking_blocks = last_assistant_with_tools.get("thinking_blocks") - return thinking_blocks is None or len(thinking_blocks) == 0 + return thinking_blocks is None or ( + hasattr(thinking_blocks, "__len__") and len(thinking_blocks) == 0 + ) def add_dummy_tool(custom_llm_provider: str) -> List[ChatCompletionToolParam]: @@ -7032,7 +7039,9 @@ def validate_chat_completion_user_messages(messages: List[AllMessageValues]): for item in user_content: if isinstance(item, dict): if item.get("type") not in ValidUserMessageContentTypes: - raise Exception(f"invalid content type={item.get('type')}") + raise Exception( + f"invalid content type={item.get('type')}" + ) except Exception as e: if isinstance(e, KeyError): raise Exception( @@ -7867,9 +7876,7 @@ class ProviderConfigManager: return GeminiVideoConfig() elif LlmProviders.VERTEX_AI == provider: - from litellm.llms.vertex_ai.videos.transformation import ( - VertexAIVideoConfig, - ) + from litellm.llms.vertex_ai.videos.transformation import VertexAIVideoConfig return VertexAIVideoConfig() elif LlmProviders.RUNWAYML == provider: diff --git a/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py b/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py index e9558580d98..13f3e7b47e7 100644 --- a/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py +++ b/tests/test_litellm/llms/openai/responses/test_openai_responses_guardrail_handler.py @@ -716,3 +716,108 @@ class TestOpenAIResponsesHandlerToolCallExtraction: assert texts_to_check[0] == "I'll check the weather for you" assert len(tool_calls_to_check) == 1 assert tool_calls_to_check[0]["function"]["name"] == "get_current_weather" + + def test_extract_text_from_basemodel_instance(self): + """Test extracting text from GenericResponseOutputItem as BaseModel instance + + This test verifies that _extract_output_text_and_images correctly handles + GenericResponseOutputItem when passed as a Pydantic BaseModel instance + (not as a dict). This addresses the issue where isinstance(output_item, BaseModel) + was failing because the handler was importing BaseModel from openai instead of pydantic. + """ + handler = OpenAIResponsesHandler() + + # Create a proper GenericResponseOutputItem instance (Pydantic BaseModel) + output_item = GenericResponseOutputItem( + type="message", + id="msg_123", + status="completed", + role="assistant", + content=[ + OutputText( + type="output_text", + text="Hi! My name is Ishaan.", + annotations=[], + ) + ], + ) + + texts_to_check: List[str] = [] + images_to_check: List[str] = [] + tool_calls_to_check: List[Any] = [] + task_mappings: List[Tuple[int, int]] = [] + + # Extract text from the BaseModel instance + handler._extract_output_text_and_images( + output_item=output_item, + output_idx=0, + texts_to_check=texts_to_check, + images_to_check=images_to_check, + task_mappings=task_mappings, + tool_calls_to_check=tool_calls_to_check, + ) + + # Verify text was extracted correctly + assert len(texts_to_check) == 1 + assert texts_to_check[0] == "Hi! My name is Ishaan." + assert len(task_mappings) == 1 + assert task_mappings[0] == (0, 0) # (output_idx, content_idx) + assert len(tool_calls_to_check) == 0 # No tool calls in this output + + def test_extract_text_from_basemodel_with_multiple_content_items(self): + """Test extracting multiple text items from GenericResponseOutputItem BaseModel + + This test verifies that the handler correctly processes a BaseModel instance + with multiple content items in the content array. + """ + handler = OpenAIResponsesHandler() + + # Create GenericResponseOutputItem with multiple content items + output_item = GenericResponseOutputItem( + type="message", + id="msg_456", + status="completed", + role="assistant", + content=[ + OutputText( + type="output_text", + text="First paragraph.", + annotations=[], + ), + OutputText( + type="output_text", + text="Second paragraph.", + annotations=[], + ), + OutputText( + type="output_text", + text="Third paragraph.", + annotations=[], + ), + ], + ) + + texts_to_check: List[str] = [] + images_to_check: List[str] = [] + tool_calls_to_check: List[Any] = [] + task_mappings: List[Tuple[int, int]] = [] + + # Extract all text items + handler._extract_output_text_and_images( + output_item=output_item, + output_idx=0, + texts_to_check=texts_to_check, + images_to_check=images_to_check, + task_mappings=task_mappings, + tool_calls_to_check=tool_calls_to_check, + ) + + # Verify all text items were extracted + assert len(texts_to_check) == 3 + assert texts_to_check[0] == "First paragraph." + assert texts_to_check[1] == "Second paragraph." + assert texts_to_check[2] == "Third paragraph." + assert len(task_mappings) == 3 + assert task_mappings[0] == (0, 0) + assert task_mappings[1] == (0, 1) + assert task_mappings[2] == (0, 2)