diff --git a/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/README.md b/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/README.md index 2563eef227e..d6ba8f635bb 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/README.md +++ b/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/README.md @@ -34,6 +34,7 @@ Each evaluate call carries `session_id` from the LiteLLM session and `consumer_i | TrustGuard `status` | LiteLLM | | --- | --- | | `block` | HTTP 400 (trace_id / request_id only; findings are not echoed) | +| `ask` | HTTP 400 like `block`: a proxy has no approval flow, so the response names `verdict: ask` | | `transform` | rewrite the last user message / last text from `transformed_payload` | | `report` / `allow` | pass through (`report` is logged by trace_id) | diff --git a/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/neuraltrust.py b/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/neuraltrust.py index d20cc3ce37c..1106f856c5b 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/neuraltrust.py +++ b/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/neuraltrust.py @@ -43,10 +43,12 @@ CONSUMER_ID_KEYS: Final = ( METADATA_ADAPTER: Final = TypeAdapter(Mapping[str, object]) EMPTY_METADATA: Final[Mapping[str, object]] = MappingProxyType({}) STATUS_BLOCK: Final = "block" +STATUS_ASK: Final = "ask" STATUS_TRANSFORM: Final = "transform" STATUS_REPORT: Final = "report" STATUS_ALLOW: Final = "allow" -KNOWN_STATUSES: Final = frozenset({STATUS_ALLOW, STATUS_BLOCK, STATUS_TRANSFORM, STATUS_REPORT}) +BLOCKING_STATUSES: Final = frozenset({STATUS_BLOCK, STATUS_ASK}) +KNOWN_STATUSES: Final = frozenset({STATUS_ALLOW, STATUS_TRANSFORM, STATUS_REPORT, *BLOCKING_STATUSES}) UNREACHABLE_HTTP_STATUSES: Final = frozenset({502, 504}) TRANSFORM_MISSING: Final = "TrustGuard transform missing payload" @@ -245,12 +247,13 @@ class NeuralTrustGuardrail(CustomGuardrail): return self._handle_unreachable(inputs, exc) status: Final = result["status"] - if status == STATUS_BLOCK: + if status in BLOCKING_STATUSES: raise HTTPException( status_code=400, detail={ # mutable-ok: FastAPI HTTPException.detail is a JSON object "error": "Violated guardrail policy", "neuraltrust_guardrail_response": "Blocked by NeuralTrust TrustGuard.", + "verdict": status, "trace_id": result.get("trace_id"), "request_id": result.get("request_id"), }, diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_neuraltrust.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_neuraltrust.py index 458987a8cd1..3476243bdb5 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_neuraltrust.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_neuraltrust.py @@ -231,12 +231,13 @@ class TestNeuralTrustGuardrail: assert "collector_key" not in mock_post.call_args.kwargs["json"] @pytest.mark.asyncio - async def test_block_raises_without_findings(self) -> None: + @pytest.mark.parametrize("status", ["block", "ask"]) + async def test_block_and_ask_raise_without_findings(self, status: str) -> None: guardrail = _guardrail() mock_post = AsyncMock( return_value=_response( { - "status": "block", + "status": status, "trace_id": "tr-1", "findings": [{"outcome": {"action": "block"}, "evidence": "ssn 123-45-6789"}], } @@ -256,6 +257,7 @@ class TestNeuralTrustGuardrail: assert "findings" not in detail assert "evidence" not in str(detail) assert detail["trace_id"] == "tr-1" + assert detail["verdict"] == status @pytest.mark.asyncio async def test_transform_rewrites_texts(self) -> None: