diff --git a/litellm/llms/openai/chat/guardrail_translation/handler.py b/litellm/llms/openai/chat/guardrail_translation/handler.py index 332285722e1..20ca6a95aab 100644 --- a/litellm/llms/openai/chat/guardrail_translation/handler.py +++ b/litellm/llms/openai/chat/guardrail_translation/handler.py @@ -214,27 +214,7 @@ class OpenAIChatCompletionsHandler(BaseTranslation): ) elif not images_to_check and not guardrail_to_apply.records_own_guardrail_information: - unscoped_texts: Final[list[str]] = [] - unscoped_tool_calls: Final[list[ChatCompletionToolParam]] = [] - for unscoped_idx, unscoped_message in enumerate(messages): - self._extract_inputs( - message=unscoped_message, - msg_idx=unscoped_idx, - texts_to_check=unscoped_texts, - images_to_check=[], - tool_calls_to_check=unscoped_tool_calls, - text_task_mappings=[], - tool_call_task_mappings=[], - ) - guardrail_to_apply.add_standard_logging_guardrail_information_to_request_data( - guardrail_json_response=( - "no scannable content after message scoping" - if unscoped_texts or unscoped_tool_calls - else "no scannable content" - ), - request_data=data, - guardrail_status="not_run", - ) + self._record_not_run(data=data, messages=messages, guardrail_to_apply=guardrail_to_apply) verbose_proxy_logger.debug( "OpenAI Chat Completions: Processed input messages: %s", @@ -243,6 +223,37 @@ class OpenAIChatCompletionsHandler(BaseTranslation): return data + def _record_not_run( + self, + data: dict, + messages: list[dict[str, Any]], + guardrail_to_apply: "CustomGuardrail", + ) -> None: + unscoped_texts: Final[list[str]] = [] + unscoped_images: Final[list[str]] = [] + unscoped_tool_calls: Final[list[ChatCompletionToolParam]] = [] + for msg_idx, message in enumerate(messages): + self._extract_inputs( + message=message, + msg_idx=msg_idx, + texts_to_check=unscoped_texts, + images_to_check=unscoped_images, + tool_calls_to_check=unscoped_tool_calls, + text_task_mappings=[], + tool_call_task_mappings=[], + ) + if unscoped_images: + return + guardrail_to_apply.add_standard_logging_guardrail_information_to_request_data( + guardrail_json_response=( + "no scannable content after message scoping" + if unscoped_texts or unscoped_tool_calls + else "no scannable content" + ), + request_data=data, + guardrail_status="not_run", + ) + def extract_request_tool_names(self, data: dict) -> list[str]: """Extract tool names from OpenAI chat completions request (tools[].function.name, functions[].name).""" names: Final[list[str]] = [] diff --git a/tests/test_litellm/llms/openai/chat/guardrail_translation/test_openai_guardrail_handler.py b/tests/test_litellm/llms/openai/chat/guardrail_translation/test_openai_guardrail_handler.py index 617fba587b2..b176d1e9057 100644 --- a/tests/test_litellm/llms/openai/chat/guardrail_translation/test_openai_guardrail_handler.py +++ b/tests/test_litellm/llms/openai/chat/guardrail_translation/test_openai_guardrail_handler.py @@ -1980,6 +1980,26 @@ class TestNoScannableContentRecordsNotRun: assert self._recorded_entries(data) == [] + @pytest.mark.asyncio + async def test_scoped_out_image_only_message_is_not_reported_as_not_run(self): + """An image in a skipped role must behave like any other image-only request""" + handler = OpenAIChatCompletionsHandler() + guardrail = MockGuardrail(guardrail_name="image-guardrail") + guardrail.skip_system_message_in_guardrail = True + data = { + "messages": [ + { + "role": "system", + "content": [{"type": "image_url", "image_url": {"url": "https://example.com/cat.png"}}], + }, + ] + } + + await handler.process_input_messages(data=data, guardrail_to_apply=guardrail) + + assert guardrail.last_inputs is None + assert self._recorded_entries(data) == [] + class ToolDroppingTextGuardrail(CustomGuardrail): """Answers one text per non-tool message it saw, the way a guardrail that