From 7596f487791270d338ade5257b6a1773f75b3c33 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:11:07 +0000 Subject: [PATCH] fix(guardrails): lower tool permission guardrail log noise The tool permission guardrail emitted expected, non-actionable events at WARNING; the no-tools skip path and every denied-by-rule message now log at DEBUG and INFO respectively so normal operation stops flooding logs --- .../guardrail_hooks/tool_permission.py | 8 +-- .../guardrail_hooks/test_tool_permission.py | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py index 2171be235e5..47f1fcef2b8 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py +++ b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py @@ -665,7 +665,7 @@ class ToolPermissionGuardrail(CustomGuardrail): new_tools = self._collect_request_tools(data) if not new_tools: - verbose_proxy_logger.warning( + verbose_proxy_logger.debug( "Tool Permission Guardrail: not running guardrail. No tools or functions in data" ) return data @@ -676,7 +676,7 @@ class ToolPermissionGuardrail(CustomGuardrail): is_allowed, _, message = self._check_tool_permission(tool_name, tool_type) if not is_allowed and message is not None: - verbose_proxy_logger.warning(f"Tool Permission Guardrail: {message}") + verbose_proxy_logger.info(f"Tool Permission Guardrail: {message}") if self.on_disallowed_action == "block": raise HTTPException( status_code=400, @@ -734,7 +734,7 @@ class ToolPermissionGuardrail(CustomGuardrail): is_allowed, rule_id, message = self._get_permission_for_tool_call(tool_call) if not is_allowed and message is not None: - verbose_proxy_logger.warning(f"Tool Permission Guardrail: {message}") + verbose_proxy_logger.info(f"Tool Permission Guardrail: {message}") if self.on_disallowed_action == "block": raise GuardrailRaisedException( @@ -813,7 +813,7 @@ class ToolPermissionGuardrail(CustomGuardrail): is_allowed, rule_id, message = self._get_permission_for_tool_call(tool_call) if not is_allowed and message is not None: - verbose_proxy_logger.warning(f"Tool Permission Guardrail: {message}") + verbose_proxy_logger.info(f"Tool Permission Guardrail: {message}") if self.on_disallowed_action == "block": raise GuardrailRaisedException( diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py index 6804ea9f8fe..cacf7c5b04c 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py @@ -3,6 +3,7 @@ Unit tests for Tool Permission Guardrail (OpenAI tool_calls semantics) """ import json +import logging import os import re import sys @@ -612,6 +613,72 @@ class TestToolPermissionGuardrail: assert excinfo.value.status_code == 400 assert excinfo.value.detail.get("detection_message") == "blocked Read by policy" + @pytest.mark.asyncio + async def test_async_pre_call_hook_no_tools_logs_at_debug(self, caplog): + user_api_key_dict = UserAPIKeyAuth() + cache = DualCache(default_in_memory_ttl=1) + + with patch.object(self.guardrail, "should_run_guardrail", return_value=True): + with caplog.at_level(logging.DEBUG, logger="LiteLLM Proxy"): + await self.guardrail.async_pre_call_hook( + user_api_key_dict=user_api_key_dict, + cache=cache, + data={}, + call_type="completion", + ) + + matching = [r for r in caplog.records if "not running guardrail" in r.message] + assert matching, "expected the no-tools message to be logged" + assert all(r.levelno == logging.DEBUG for r in matching) + + @pytest.mark.asyncio + async def test_async_pre_call_hook_denied_tool_logs_at_info(self, caplog): + data = {"tools": [{"type": "function", "function": {"name": "Read"}}]} + user_api_key_dict = UserAPIKeyAuth() + cache = DualCache(default_in_memory_ttl=1) + + with patch.object(self.guardrail, "should_run_guardrail", return_value=True): + with caplog.at_level(logging.DEBUG, logger="LiteLLM Proxy"): + with pytest.raises(HTTPException): + await self.guardrail.async_pre_call_hook( + user_api_key_dict=user_api_key_dict, + cache=cache, + data=data, + call_type="completion", + ) + + matching = [ + r + for r in caplog.records + if r.message.startswith("Tool Permission Guardrail:") + and "denied by rule" in r.message + ] + assert matching, "expected the denied-by-rule message to be logged" + assert all(r.levelno == logging.INFO for r in matching) + + @pytest.mark.asyncio + async def test_async_post_call_success_hook_denied_tool_logs_at_info(self, caplog): + tool_call = {"function": {"name": "Read", "arguments": "{}"}, "type": "function"} + response = ModelResponse(choices=[Choices(message={"tool_calls": [tool_call]})]) + user_api_key_dict = UserAPIKeyAuth() + data = {"guardrails": ["test-tool-permission"]} + + with patch.object(self.guardrail, "should_run_guardrail", return_value=True): + with caplog.at_level(logging.DEBUG, logger="LiteLLM Proxy"): + with pytest.raises(GuardrailRaisedException): + await self.guardrail.async_post_call_success_hook( + data=data, user_api_key_dict=user_api_key_dict, response=response + ) + + matching = [ + r + for r in caplog.records + if r.message.startswith("Tool Permission Guardrail:") + and "denied by rule" in r.message + ] + assert matching, "expected the denied-by-rule message to be logged" + assert all(r.levelno == logging.INFO for r in matching) + @pytest.mark.asyncio async def test_async_pre_call_hook_rewrite_mode(self): guardrail = ToolPermissionGuardrail(