fix(openai/responses/guardrail_translation): fix basemodel import (#17977)

* fix(openai/responses/guardrail_translation): fix basemodel import

ensures /v1/responses output is correctly processed by guardrail handler

* fix: fix linting error

* fix: fix linting errors
This commit is contained in:
Krish Dholakia 2025-12-15 16:13:45 +05:30 committed by GitHub
parent 6ddc7875ea
commit a3f5a94784
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 120 additions and 7 deletions

View file

@ -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:

View file

@ -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:

View file

@ -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)