From 1458e881e555f95f9435a9f3a744b79b0fa2b350 Mon Sep 17 00:00:00 2001 From: r-bit-rry Date: Thu, 20 Nov 2025 13:00:02 +0200 Subject: [PATCH 1/3] fix(nvidia-safety): correct NeMo Guardrails API endpoint --- .../providers/remote/safety/nvidia/nvidia.py | 15 ++----- tests/unit/providers/nvidia/test_safety.py | 39 +++---------------- 2 files changed, 9 insertions(+), 45 deletions(-) diff --git a/src/llama_stack/providers/remote/safety/nvidia/nvidia.py b/src/llama_stack/providers/remote/safety/nvidia/nvidia.py index 43ff45cc9..bb6fcee1b 100644 --- a/src/llama_stack/providers/remote/safety/nvidia/nvidia.py +++ b/src/llama_stack/providers/remote/safety/nvidia/nvidia.py @@ -125,7 +125,7 @@ class NeMoGuardrails: async def run(self, messages: list[OpenAIMessageParam]) -> RunShieldResponse: """ - Queries the /v1/guardrails/checks endpoint of the NeMo guardrails deployed API. + Queries the /v1/chat/completions endpoint of the NeMo guardrails deployed API. Args: messages (List[Message]): A list of Message objects to be checked for safety violations. @@ -138,19 +138,10 @@ class NeMoGuardrails: requests.HTTPError: If the POST request fails. """ request_data = { - "model": self.model, + "config_id": self.config_id, "messages": [{"role": message.role, "content": message.content} for message in messages], - "temperature": self.temperature, - "top_p": 1, - "frequency_penalty": 0, - "presence_penalty": 0, - "max_tokens": 160, - "stream": False, - "guardrails": { - "config_id": self.config_id, - }, } - response = await self._guardrails_post(path="/v1/guardrail/checks", data=request_data) + response = await self._guardrails_post(path="/v1/chat/completions", data=request_data) if response["status"] == "blocked": user_message = "Sorry I cannot do this." diff --git a/tests/unit/providers/nvidia/test_safety.py b/tests/unit/providers/nvidia/test_safety.py index 07e04ddea..b99e4dd88 100644 --- a/tests/unit/providers/nvidia/test_safety.py +++ b/tests/unit/providers/nvidia/test_safety.py @@ -152,22 +152,13 @@ async def test_run_shield_allowed(nvidia_adapter, mock_guardrails_post): # Verify the Guardrails API was called correctly mock_guardrails_post.assert_called_once_with( - path="/v1/guardrail/checks", + path="/v1/chat/completions", data={ - "model": shield_id, + "config_id": "self-check", "messages": [ {"role": "user", "content": "Hello, how are you?"}, {"role": "assistant", "content": "I'm doing well, thank you for asking!"}, ], - "temperature": 1.0, - "top_p": 1, - "frequency_penalty": 0, - "presence_penalty": 0, - "max_tokens": 160, - "stream": False, - "guardrails": { - "config_id": "self-check", - }, }, ) @@ -206,22 +197,13 @@ async def test_run_shield_blocked(nvidia_adapter, mock_guardrails_post): # Verify the Guardrails API was called correctly mock_guardrails_post.assert_called_once_with( - path="/v1/guardrail/checks", + path="/v1/chat/completions", data={ - "model": shield_id, + "config_id": "self-check", "messages": [ {"role": "user", "content": "Hello, how are you?"}, {"role": "assistant", "content": "I'm doing well, thank you for asking!"}, ], - "temperature": 1.0, - "top_p": 1, - "frequency_penalty": 0, - "presence_penalty": 0, - "max_tokens": 160, - "stream": False, - "guardrails": { - "config_id": "self-check", - }, }, ) @@ -286,22 +268,13 @@ async def test_run_shield_http_error(nvidia_adapter, mock_guardrails_post): # Verify the Guardrails API was called correctly mock_guardrails_post.assert_called_once_with( - path="/v1/guardrail/checks", + path="/v1/chat/completions", data={ - "model": shield_id, + "config_id": "self-check", "messages": [ {"role": "user", "content": "Hello, how are you?"}, {"role": "assistant", "content": "I'm doing well, thank you for asking!"}, ], - "temperature": 1.0, - "top_p": 1, - "frequency_penalty": 0, - "presence_penalty": 0, - "max_tokens": 160, - "stream": False, - "guardrails": { - "config_id": "self-check", - }, }, ) # Verify the exception message From c580bae3694d7e53a5c0ecab0ee2dec15eaddbe2 Mon Sep 17 00:00:00 2001 From: r-bit-rry Date: Mon, 24 Nov 2025 22:16:50 +0200 Subject: [PATCH 2/3] further changes to nvidia.py --- .../providers/remote/safety/nvidia/nvidia.py | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/llama_stack/providers/remote/safety/nvidia/nvidia.py b/src/llama_stack/providers/remote/safety/nvidia/nvidia.py index bb6fcee1b..9e24dd109 100644 --- a/src/llama_stack/providers/remote/safety/nvidia/nvidia.py +++ b/src/llama_stack/providers/remote/safety/nvidia/nvidia.py @@ -131,8 +131,7 @@ class NeMoGuardrails: messages (List[Message]): A list of Message objects to be checked for safety violations. Returns: - RunShieldResponse: If the response indicates a violation ("blocked" status), returns a - RunShieldResponse with a SafetyViolation; otherwise, returns a RunShieldResponse with violation set to None. + RunShieldResponse: Response with SafetyViolation if content is blocked, None otherwise. Raises: requests.HTTPError: If the POST request fails. @@ -143,16 +142,38 @@ class NeMoGuardrails: } response = await self._guardrails_post(path="/v1/chat/completions", data=request_data) - if response["status"] == "blocked": - user_message = "Sorry I cannot do this." - metadata = response["rails_status"] - + # Support legacy format with explicit status field + if "status" in response and response["status"] == "blocked": return RunShieldResponse( violation=SafetyViolation( - user_message=user_message, + user_message="Sorry I cannot do this.", violation_level=ViolationLevel.ERROR, - metadata=metadata, + metadata=response.get("rails_status", {}), ) ) - return RunShieldResponse(violation=None) + # NOTE: The implementation targets the actual behavior of the NeMo Guardrails server + # as defined in 'nemoguardrails/server/api.py'. The 'RequestBody' class accepts + # 'config_id' at the top level, and 'ResponseBody' returns a 'messages' array, + # distinct from the OpenAI 'choices' format often referenced in documentation. + + response_messages = response.get("messages", []) + if response_messages: + content = response_messages[0].get("content", "").strip() + else: + choices = response.get("choices", []) + if choices: + content = choices[0].get("message", {}).get("content", "").strip() + else: + content = "" + + refusal_phrases = ["sorry i cannot do this", "i cannot help with that", "i can't assist with that"] + is_blocked = not content or any(phrase in content.lower() for phrase in refusal_phrases) + + return RunShieldResponse( + violation=SafetyViolation( + user_message="Sorry I cannot do this.", + violation_level=ViolationLevel.ERROR, + metadata={"reason": "Content violates safety guidelines", "response": content or "(empty)"}, + ) if is_blocked else None + ) From 4d660413e038b11092637dda0c5a81c83a982ad8 Mon Sep 17 00:00:00 2001 From: r-bit-rry Date: Tue, 25 Nov 2025 10:57:20 +0200 Subject: [PATCH 3/3] precommit hook check and fixes --- src/llama_stack/providers/remote/safety/nvidia/nvidia.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/llama_stack/providers/remote/safety/nvidia/nvidia.py b/src/llama_stack/providers/remote/safety/nvidia/nvidia.py index 9e24dd109..d217647a4 100644 --- a/src/llama_stack/providers/remote/safety/nvidia/nvidia.py +++ b/src/llama_stack/providers/remote/safety/nvidia/nvidia.py @@ -175,5 +175,7 @@ class NeMoGuardrails: user_message="Sorry I cannot do this.", violation_level=ViolationLevel.ERROR, metadata={"reason": "Content violates safety guidelines", "response": content or "(empty)"}, - ) if is_blocked else None + ) + if is_blocked + else None )