diff --git a/litellm/proxy/guardrails/guardrail_hooks/llm_as_a_judge/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/llm_as_a_judge/__init__.py index 46fccc126e7..8eac6b2ee53 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/llm_as_a_judge/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/llm_as_a_judge/__init__.py @@ -78,7 +78,10 @@ class _LoggedCallParams(BaseModel): metadata: Mapping[str, object] | None = None -def _is_judge_call(data: Mapping[str, object]) -> bool: +def _is_logged_judge_call(data: Mapping[str, object], event_type: GuardrailEventHooks) -> bool: + """logging_only is the only event whose ``data`` is the SDK's model_call_details rather than the client body.""" + if event_type is not GuardrailEventHooks.logging_only: + return False try: params: Final = _LoggedCallParams.model_validate(data.get("litellm_params") or {}) except ValidationError: @@ -207,7 +210,7 @@ class LLMAsAJudgeGuardrail(CustomGuardrail): return [GuardrailEventHooks.pre_call, GuardrailEventHooks.during_call, GuardrailEventHooks.post_call] def should_run_guardrail(self, data: Mapping[str, object], event_type: GuardrailEventHooks) -> bool: - if _is_judge_call(data): + if _is_logged_judge_call(data, event_type): return False return super().should_run_guardrail(data, event_type) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 47c78bcc4c8..7a78e055092 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -2671,7 +2671,7 @@ class ProxyLogging: async def _run_during_call_guardrail( self, callback: CustomGuardrail, - data: dict, + data: dict[str, object], # mutable-ok: request payload dict, guardrail_to_apply is written in place user_api_key_dict: UserAPIKeyAuth | None, user_api_key_auth_dict: UserAPIKeyAuth | dict[str, object] | None, call_type: CallTypesLiteral, diff --git a/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py b/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py index 1646f730984..6e00958eba4 100644 --- a/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py +++ b/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py @@ -8,6 +8,7 @@ import pytest from fastapi import HTTPException import litellm +from litellm.constants import INTERNAL_CALL_ORIGIN_METADATA_KEY from litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge import ( LLMAsAJudgeGuardrail, _build_judge_prompt, @@ -16,6 +17,7 @@ from litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge import ( initialize_guardrail, ) from litellm.types.guardrails import GuardrailEventHooks, Mode +from litellm.types.utils import LLM_AS_A_JUDGE_GUARDRAIL_CALL_ORIGIN # --------------------------------------------------------------------------- # Helpers @@ -420,6 +422,20 @@ async def test_logging_only_judge_does_not_judge_its_own_judge_call(): assert guardrail.should_run_guardrail(client_call, GuardrailEventHooks.logging_only) is True +@pytest.mark.parametrize( + "event_type", [GuardrailEventHooks.pre_call, GuardrailEventHooks.during_call, GuardrailEventHooks.post_call] +) +def test_client_supplied_judge_origin_does_not_bypass_enforcing_hooks(event_type: GuardrailEventHooks): + guardrail: Final = _make_guardrail(event_hook=event_type) + forged_request: Final[dict[str, object]] = { + "messages": [{"role": "user", "content": "hi"}], + "guardrails": [guardrail.guardrail_name], + "litellm_params": {"metadata": {INTERNAL_CALL_ORIGIN_METADATA_KEY: LLM_AS_A_JUDGE_GUARDRAIL_CALL_ORIGIN}}, + } + + assert guardrail.should_run_guardrail(forged_request, event_type) is True + + @pytest.mark.asyncio async def test_apply_guardrail_response_prompt_unchanged(): router: Final = _judge_router(90.0)