diff --git a/litellm/proxy/guardrails/guardrail_hooks/ovalix/ovalix.py b/litellm/proxy/guardrails/guardrail_hooks/ovalix/ovalix.py index 756ac150314..74d4e291666 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/ovalix/ovalix.py +++ b/litellm/proxy/guardrails/guardrail_hooks/ovalix/ovalix.py @@ -196,12 +196,12 @@ class OvalixGuardrail(CustomGuardrail): return metadata["user_api_key_user_email"] if metadata.get("user_api_key_user_id"): return metadata["user_api_key_user_id"] - return "unknown" + return "" def _get_tracker_actor_id(self, data: dict) -> str: """Normalize the actor string into a short, stable id for Tracker API payloads.""" # NOTE: this hash is purely for normalization — it collapses an arbitrary actor - # string (email, user id, or "unknown") into a compact, fixed-length, consistent + # string (email, user id, or empty) into a compact, fixed-length, consistent # key. It is not a privacy/security measure and the actor value is not sensitive, # so a plain SHA-256 (truncated) is sufficient; no salting/KDF is needed here. actor_id = self._get_actor(data).encode() @@ -311,7 +311,7 @@ class OvalixGuardrail(CustomGuardrail): logging_obj: Optional[Any] = None, ) -> GenericGuardrailAPIInputs: routing = await self._resolve_routing(request_data) - actor = self._get_tracker_actor_id(request_data) + actor = self._get_actor(request_data) session_id = self._get_session_id_for_application(request_data, routing.application_id) is_response = input_type == "response" diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_ovalix.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_ovalix.py index 4c656896c3d..1342dd5cbe7 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_ovalix.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_ovalix.py @@ -590,7 +590,7 @@ class TestOvalixGuardrail: assert guardrail._get_actor({"metadata": {"user_api_key_user_email": "a@b.com"}}) == "a@b.com" assert guardrail._get_actor({"metadata": {"user_api_key_user_id": "uid-1"}}) == "uid-1" assert guardrail._get_actor({"litellm_metadata": {"user_api_key_user_id": "uid-2"}}) == "uid-2" - assert guardrail._get_actor({}) == "unknown" + assert guardrail._get_actor({}) == "" finally: for k in _ovalix_env(): if k in os.environ: @@ -994,6 +994,45 @@ async def test_all_allow_passes_through(): assert result["texts"] == ["hi"] +@pytest.mark.asyncio +async def test_actor_sent_to_tracker_is_raw_identifier_not_hash(): + g = _static_guardrail() + request_data = {"metadata": {"user_api_key_user_email": "user@example.com"}} + inputs = GenericGuardrailAPIInputs(texts=["hi"]) + seen = {} + + async def _post(url, headers=None, json=None): + seen["last"] = json + r = MagicMock() + r.json.return_value = _ALLOW + r.raise_for_status = MagicMock() + return r + + with patch.object(g._async_handler, "post", new=_post): + await g.apply_guardrail(inputs=inputs, request_data=request_data, input_type="request", logging_obj=None) + assert seen["last"]["actor"] == "user@example.com" + assert seen["last"]["session_id"] != "user@example.com" + assert g._get_tracker_actor_id(request_data) in seen["last"]["session_id"] + + +@pytest.mark.asyncio +async def test_empty_user_sends_empty_actor_matching_reference(): + g = _static_guardrail() + inputs = GenericGuardrailAPIInputs(texts=["hi"]) + seen = {} + + async def _post(url, headers=None, json=None): + seen["last"] = json + r = MagicMock() + r.json.return_value = _ALLOW + r.raise_for_status = MagicMock() + return r + + with patch.object(g._async_handler, "post", new=_post): + await g.apply_guardrail(inputs=inputs, request_data={}, input_type="request", logging_obj=None) + assert seen["last"]["actor"] == "" + + @pytest.mark.asyncio async def test_tool_result_content_skipped_on_text_path(): g = _static_guardrail()