From 76c02ea7e910ed7315e3932b4e45a64d7cd55c7a Mon Sep 17 00:00:00 2001 From: yucheng Date: Tue, 29 Sep 2026 08:25:44 +0000 Subject: [PATCH] fix(guardrails): allow native lifecycle logging-only scope Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/integrations/custom_guardrail.py | 1 - .../guardrails/test_guardrail_registry.py | 19 ++++++++++++++++++- .../integrations/test_custom_guardrail.py | 11 +++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index ffe9544007c..2407c0032a3 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -809,7 +809,6 @@ class CustomGuardrail(CustomLogger): def supports_logging_only_scope(self) -> bool: return ( self.uses_apply_guardrail_interface() - and not self.use_native_lifecycle_hooks and type(self).async_logging_hook is CustomGuardrail.async_logging_hook ) diff --git a/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py b/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py index 6af7a7a06a4..45e652bff72 100644 --- a/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py +++ b/tests/test_litellm/proxy/guardrails/test_guardrail_registry.py @@ -1,5 +1,5 @@ from collections.abc import Iterable -from typing import Final +from typing import ClassVar, Final from unittest.mock import AsyncMock, MagicMock import pytest @@ -956,6 +956,10 @@ class _LoggingOnlyScopeUnsupportedGuardrail(_LoggingOnlyScopeSupportedGuardrail) return kwargs, result +class _LoggingOnlyScopeNativeGuardrail(_LoggingOnlyScopeSupportedGuardrail): + use_native_lifecycle_hooks: ClassVar[bool] = True + + class TestLoggingOnlyScopeValidation: def _initialize( self, @@ -968,9 +972,13 @@ class TestLoggingOnlyScopeValidation: guardrail_type: Final = "logging_only_scope_test" def _initializer(litellm_params: LitellmParams, guardrail: Guardrail) -> CustomGuardrail: + supported_event_hooks: Final = ( + [GuardrailEventHooks.logging_only] if callback_type.use_native_lifecycle_hooks else None + ) return callback_type( guardrail_name=guardrail["guardrail_name"], event_hook=litellm_params.mode, + supported_event_hooks=supported_event_hooks, ) registry_module.guardrail_initializer_registry[guardrail_type] = _initializer @@ -1041,6 +1049,15 @@ class TestLoggingOnlyScopeValidation: assert callback.logging_only_scope == "both" + def test_output_scope_accepted_for_native_lifecycle_guardrail(self) -> None: + callback: Final = self._initialize( + mode="logging_only", + scope="output", + callback_type=_LoggingOnlyScopeNativeGuardrail, + ) + + assert callback.logging_only_scope == "output" + def test_invalid_scope_fails_litellm_params_validation(self) -> None: with pytest.raises(ValidationError): LitellmParams(guardrail="test", mode="logging_only", logging_only_scope="request") diff --git a/tests/unit/integrations/test_custom_guardrail.py b/tests/unit/integrations/test_custom_guardrail.py index d01b54aa953..052eb347b9c 100644 --- a/tests/unit/integrations/test_custom_guardrail.py +++ b/tests/unit/integrations/test_custom_guardrail.py @@ -2961,6 +2961,17 @@ class _NativeLifecycleLoggingGuardrail(CustomGuardrail): return inputs +@pytest.mark.asyncio +async def test_native_lifecycle_guardrail_logging_only_scope_scans_only_input(): + guardrail: Final = _NativeLifecycleLoggingGuardrail() + guardrail.logging_only_scope = "input" + kwargs, response = _logged_call([{"role": "user", "content": "native lifecycle input"}]) + + await guardrail.async_logging_hook(kwargs, response, CallTypes.acompletion.value) + + assert guardrail.calls == [("request", ["native lifecycle input"])] + + @pytest.mark.asyncio async def test_native_lifecycle_guardrail_logging_only_scans_assembled_response(): """A use_native_lifecycle_hooks guardrail accepts mode logging_only and its