-
-
Notifications
You must be signed in to change notification settings - Fork 11.7k
feat(guardrails): optional skip tool message in unified guardrail inputs #27441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,9 @@ | |
| from litellm.llms.base_llm.guardrail_translation.base_translation import BaseTranslation | ||
| from litellm.llms.base_llm.guardrail_translation.utils import ( | ||
| effective_skip_system_message_for_guardrail, | ||
| effective_skip_tool_message_for_guardrail, | ||
| openai_messages_without_system, | ||
| openai_messages_without_tool, | ||
| ) | ||
| from litellm.llms.openai.chat.guardrail_translation.handler import ( | ||
| OpenAIChatCompletionsHandler, | ||
|
|
@@ -180,6 +182,136 @@ async def apply_guardrail( | |
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The PR mirrors |
||
| assert "system" in roles | ||
|
|
||
| class TestSkipToolMessageForChatCompletions: | ||
| def test_openai_messages_without_tool(self): | ||
| msgs = [ | ||
| {"role": "user", "content": "hi"}, | ||
| { | ||
| "role": "assistant", | ||
| "content": None, | ||
| "tool_calls": [ | ||
| { | ||
| "id": "call_1", | ||
| "type": "function", | ||
| "function": {"name": "f", "arguments": "{}"}, | ||
| } | ||
| ], | ||
| }, | ||
| {"role": "tool", "content": "tool result", "tool_call_id": "call_1"}, | ||
| ] | ||
| out = openai_messages_without_tool(msgs) | ||
| assert len(out) == 2 | ||
| assert all(m["role"] != "tool" for m in out) | ||
| assert msgs[2]["content"] == "tool result" | ||
|
|
||
| def test_effective_skip_tool_respects_per_guardrail_over_global( | ||
| self, monkeypatch | ||
| ): | ||
| monkeypatch.setattr( | ||
| litellm, "skip_tool_message_in_guardrail", True, raising=False | ||
| ) | ||
|
|
||
| class G: | ||
| skip_tool_message_in_guardrail = False | ||
|
|
||
| assert effective_skip_tool_message_for_guardrail(G()) is False | ||
|
|
||
| class G2: | ||
| skip_tool_message_in_guardrail = None | ||
|
|
||
| assert effective_skip_tool_message_for_guardrail(G2()) is True | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_openai_handler_skips_tool_in_guardrail_inputs(self, monkeypatch): | ||
| monkeypatch.setattr( | ||
| litellm, "skip_tool_message_in_guardrail", True, raising=False | ||
| ) | ||
|
|
||
| captured = {} | ||
|
|
||
| class MockGuardrail: | ||
| skip_tool_message_in_guardrail = None | ||
|
|
||
| async def apply_guardrail( | ||
| self, inputs, request_data, input_type, logging_obj=None | ||
| ): | ||
| captured["inputs"] = inputs | ||
| return inputs | ||
|
|
||
| data = { | ||
| "messages": [ | ||
| {"role": "user", "content": "hello"}, | ||
| { | ||
| "role": "assistant", | ||
| "content": None, | ||
| "tool_calls": [ | ||
| { | ||
| "id": "call_1", | ||
| "type": "function", | ||
| "function": {"name": "f", "arguments": "{}"}, | ||
| } | ||
| ], | ||
| }, | ||
| { | ||
| "role": "tool", | ||
| "content": "secret tool result", | ||
| "tool_call_id": "call_1", | ||
| }, | ||
| ], | ||
| "model": "gpt-4o", | ||
| } | ||
|
|
||
| handler = OpenAIChatCompletionsHandler() | ||
| await handler.process_input_messages( | ||
| data=data, | ||
| guardrail_to_apply=MockGuardrail(), | ||
| litellm_logging_obj=None, | ||
| ) | ||
|
|
||
| assert "secret tool result" not in captured["inputs"]["texts"] | ||
| sm = captured["inputs"].get("structured_messages") or [] | ||
| assert all(m.get("role") != "tool" for m in sm) | ||
| assert data["messages"][2]["content"] == "secret tool result" | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_openai_handler_per_guardrail_skip_tool_false_overrides_global( | ||
| self, monkeypatch | ||
| ): | ||
| monkeypatch.setattr( | ||
| litellm, "skip_tool_message_in_guardrail", True, raising=False | ||
| ) | ||
|
|
||
| captured = {} | ||
|
|
||
| class MockGuardrail: | ||
| skip_tool_message_in_guardrail = False | ||
|
|
||
| async def apply_guardrail( | ||
| self, inputs, request_data, input_type, logging_obj=None | ||
| ): | ||
| captured["inputs"] = inputs | ||
| return inputs | ||
|
|
||
| data = { | ||
| "messages": [ | ||
| {"role": "user", "content": "u"}, | ||
| {"role": "tool", "content": "tr", "tool_call_id": "call_1"}, | ||
| ], | ||
| } | ||
|
|
||
| await OpenAIChatCompletionsHandler().process_input_messages( | ||
| data=data, | ||
| guardrail_to_apply=MockGuardrail(), | ||
| litellm_logging_obj=None, | ||
| ) | ||
|
|
||
| assert "tr" in captured["inputs"]["texts"] | ||
| roles = { | ||
| m.get("role") | ||
| for m in (captured["inputs"].get("structured_messages") or []) | ||
| } | ||
| assert "tool" in roles | ||
|
|
||
| class TestAsyncPreCallHook: | ||
| @pytest.mark.asyncio | ||
| async def test_uses_mcp_event_type(self): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skip_tool_messageguard is dead code for Anthropic native tool resultsIn the Anthropic Messages passthrough flow,
data["messages"]is in Anthropic native format. Tool results are represented asuser-role messages containingtype: "tool_result"content blocks — they never carryrole: "tool". The guardif skip_tool_message and role == "tool": returntherefore never fires for these messages, making the skip ineffective at the extraction level.The effective behavior happens to be correct today because
_extract_input_text_and_imagesonly pullscontent_item.get("text"), which returnsNonefortool_resultblocks (they usecontent, nottext). Thestructured_messagesfilter also works correctly because it operates on the already-translated OpenAI-format messages. However, if text extraction is ever enhanced to handle nestedtool_resultcontent blocks, this guard will silently fail to suppress them.