fix(guardrails): leave scoped-out image-only input unrecorded and split the not_run helper

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-15 05:10:32 +00:00
parent 9acdebf563
commit 714b113c5f
2 changed files with 52 additions and 21 deletions

View file

@ -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]] = []

View file

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