Add native Vigil Guard guardrail provider#29339
Conversation
Add a native Vigil Guard guardrail provider for the LiteLLM proxy. It scans chat input (pre_call) and model output (post_call), maps Vigil Guard ALLOWED/SANITIZED/BLOCKED decisions onto the standard guardrail flow, and supports configurable fail-open or fail-closed behavior for backend and transport failures (default fail_closed via unreachable_fallback). Adds the provider, its UI config model, the integration enum, and focused tests.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR adds a native Vigil Guard guardrail provider for the LiteLLM proxy and fixes a pre-existing bug in the OpenAI chat guardrail translation handler where guardrail-returned tool call lists were silently discarded in favour of the original inputs.
Confidence Score: 5/5Safe to merge; the new provider is well-isolated and the handler change is a targeted, regression-tested fix that improves correctness for all guardrails on the tool-call output path. The handler fix is correct and backed by a dedicated regression test. The Vigil Guard provider follows established patterns, uses litellm's existing HTTP client infrastructure, passes secrets only in the Authorization header, and has comprehensive mock-only test coverage. Open concerns from earlier review rounds are acknowledged but no new blocking issues were found. litellm/proxy/guardrails/guardrail_hooks/vigil_guard/vigil_guard.py — specifically the _handle_backend_failure fail_closed path and the partial-sanitization behaviour on mid-loop backend failure, both of which were raised in earlier review threads.
|
| Filename | Overview |
|---|---|
| litellm/llms/openai/chat/guardrail_translation/handler.py | Fixes a regression where guardrail-returned tool calls were discarded if the guardrail returned a new list rather than mutating in-place; now falls back to the original list only when the guardrail omits the key entirely. |
| litellm/proxy/guardrails/guardrail_hooks/vigil_guard/vigil_guard.py | New Vigil Guard guardrail provider with pre/post-call scanning, tool-call argument scanning, metadata collection, and fail-open/fail-closed backend failure handling; the fail_closed path in _handle_backend_failure raises raw httpx/timeout exceptions instead of GuardrailRaisedException (surfacing as HTTP 500 instead of the expected guardrail 400). |
| litellm/proxy/guardrails/guardrail_hooks/vigil_guard/init.py | Registers VigilGuardGuardrail in the guardrail initializer and class registries; follows the same pattern as existing providers. |
| litellm/types/guardrails.py | Adds VIGIL_GUARD to SupportedGuardrailIntegrations enum and VigilGuardGuardrailConfigModel to the LitellmParams multiple-inheritance chain; consistent with other provider additions. |
| litellm/types/proxy/guardrails/guardrail_hooks/vigil_guard.py | New Pydantic config model for Vigil Guard with api_base and api_key fields; follows the same pattern as HiddenlayerGuardrailConfigModel and QostodianNexusConfigModel. |
| tests/test_litellm/proxy/guardrails/guardrail_hooks/test_vigil_guard.py | Comprehensive mock-only test suite covering allowed/sanitized/blocked decisions, metadata handling, retry logic, fail-open/fail-closed modes, tool-call scanning, and registry wiring; no real network calls. |
| tests/test_litellm/llms/openai/chat/guardrail_translation/test_openai_guardrail_handler.py | Adds MockCopiedToolCallGuardrail and a regression test verifying that returned tool calls are remapped even when the guardrail returns a new list instead of mutating in-place; unused imports cleaned up. |
Reviews (5): Last reviewed commit: "fix(guardrails): apply guardrail-returne..." | Re-trigger Greptile
Greptile SummaryThis PR adds a native Vigil Guard guardrail provider that integrates into the existing LiteLLM guardrail hook pipeline, supporting
Confidence Score: 4/5Safe to merge; the two findings are edge-case behaviors in optional fail-open mode and an obscure Python type subclassing quirk, neither of which affects the critical request path or default configuration. The implementation is well-structured and the test suite is thorough. The main nuance worth noting is that in fail-open mode, a backend failure mid-way through scanning multiple texts silently reverts the sanitization already applied to earlier texts, since the method returns the original unmodified inputs. The boolean-forwarding in metadata is explicitly documented by tests, but could cause unexpected type errors against the Vigil Guard API. vigil_guard.py — specifically the apply_guardrail mid-loop failure path and the _clamp_metadata_value boolean handling.
|
| Filename | Overview |
|---|---|
| litellm/proxy/guardrails/guardrail_hooks/vigil_guard/vigil_guard.py | Core guardrail implementation: pre/post call scanning, retry-once on transient errors, fail-open/fail-closed modes, metadata collection with allowlist clamping, and BLOCKED/SANITIZED/ALLOWED decision handling. Two style-level concerns: fail-open silently reverts previously sanitized texts on mid-loop failures, and booleans pass through the numeric metadata clamp. |
| litellm/proxy/guardrails/guardrail_hooks/vigil_guard/init.py | Module entry-point: initializes the guardrail, registers it in the initializer and class registries, and wires into logging_callback_manager — consistent with the pattern used by other guardrail hooks. |
| litellm/types/guardrails.py | Adds VIGIL_GUARD = 'vigil_guard' to SupportedGuardrailIntegrations and adds VigilGuardGuardrailConfigModel to the LitellmParams multiple-inheritance chain, matching the pattern for all other guardrail providers. |
| litellm/types/proxy/guardrails/guardrail_hooks/vigil_guard.py | Pydantic config model for Vigil Guard: api_base and api_key fields with env-var fallback documentation and a ui_friendly_name. Clean and minimal. |
| tests/test_litellm/proxy/guardrails/guardrail_hooks/test_vigil_guard.py | 72 fully mocked unit tests covering allowed/sanitized/blocked decisions, sanitized-text precedence, block-reason precedence, retry logic, fail-open/fail-closed modes, metadata allowlist and clamping, multi-text ordering, and registry wiring. No real network calls. |
Reviews (2): Last reviewed commit: "feat(guardrails): add native vigil_guard..." | Re-trigger Greptile
Preserve already-applied sanitization in fail_open multi-text scans: on a mid-loop backend failure, return the scanned texts plus the remaining unscanned texts instead of the original inputs, so completed redactions are not reverted and PII is not reintroduced. Raise GuardrailRaisedException (400) for an unrecognized decision under fail_closed, instead of letting VigilGuardBackendError surface as an opaque 500; the raw decision value stays in the server log only. Exclude booleans from forwarded metadata so string-typed fields are not sent a bool value. Adds a multi-text fail_open regression test; provider patch coverage is 100%.
|
Thanks for the review. Addressed all three code findings in 1b0845e. 1. fail_open partial sanitization (P1). On a mid-loop backend failure in fail_open mode, 2. Unrecognized decision in fail_closed (P2). This path now raises 3. Boolean metadata. Provider patch coverage is 100% and the full guardrails bucket passes (1690 tests). |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.
Reviewed by Cursor Bugbot for commit 1b0845e. Configure here.
| ) -> GenericGuardrailAPIInputs: | ||
| guardrailed = dict(inputs) | ||
| guardrailed["texts"] = list(scanned) + list(remaining) | ||
| return cast(GenericGuardrailAPIInputs, guardrailed) |
There was a problem hiding this comment.
Fail-open path returns inconsistent output shape
Low Severity
_fail_open_passthrough uses dict(inputs) which preserves all original keys (including structured_messages, model, tool_calls), while the normal successful path at line 197–202 only returns texts, images, and tools. This means the _inputs_were_modified comparison in the log_guardrail_information decorator produces different guardrail-logging outcomes depending on whether the backend was reachable: the normal ALLOWED path always logs "mask" (because dropped keys like structured_messages cause a mismatch with original_inputs), while the fail-open path correctly logs "allow" when texts are unmodified.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 1b0845e. Configure here.
There was a problem hiding this comment.
Addressed in 6b217cc. apply_guardrail now returns the input shape verbatim (via _build_output) whenever the scanned texts are unchanged, so a clean ALLOWED request logs allow rather than mask and the success and fail_open paths are consistent. When any text was actually sanitized it returns only the remap-relevant keys (texts, images, tools) and intentionally drops structured_messages, so a stale unsanitized payload cannot reach the model even where structured_messages would take precedence. Added telemetry assertions for the allow path (clean ALLOWED, clean fail_open) and the mask path (SANITIZED, partial fail_open after sanitization).
| input_type: Literal["request", "response"], | ||
| logging_obj: Optional["LiteLLMLoggingObj"] = None, | ||
| ) -> GenericGuardrailAPIInputs: | ||
| texts = inputs.get("texts") or [] |
There was a problem hiding this comment.
Medium: Tool content bypasses the guardrail
The guardrail framework passes tool schemas and tool_calls in inputs, but this implementation only analyzes texts and returns immediately when there are no non-empty text entries. A caller can place disallowed content in messages[].tool_calls[].function.arguments or in tool descriptions and have it reach the model or downstream tool consumer without Vigil seeing it; serialize those fields for analysis or fail closed when unscanned structured inputs are present.
There was a problem hiding this comment.
Addressed in 1f1f7fa. apply_guardrail now scans model-generated tool-call arguments on the response path: each non-empty tool_calls[].function.arguments is analyzed with source=model_output. ALLOWED leaves the call unchanged, BLOCKED raises the guardrail 400, and SANITIZED replaces only that call's arguments on a copied tool_calls list without mutating the input. This closes the bypass for disallowed content in model-generated tool-call arguments. Static request tool schemas/descriptions are intentionally out of scope for this PR; backend failures during tool-call scanning follow the existing fail_closed / fail_open behavior. Added tests for the allowed, sanitized, blocked, unrecognized-decision, and backend-failure (fail_closed and fail_open) paths, plus a check that request-side tool_calls are not scanned. Patch coverage stays at 100%.
PR overviewThis pull request adds a native Vigil Guard guardrail provider for the LiteLLM proxy, wiring a new guardrail hook implementation into the guardrails framework. The touched code focuses on collecting request content and sending it to Vigil Guard for evaluation. There is one open security concern remaining: the current implementation only scans plain text entries and can miss structured tool-related content such as tool schemas or tool call arguments. That leaves a path for disallowed content embedded in tool fields to avoid inspection before reaching the model or downstream tool handling. One prior issue has already been addressed, so the review is trending down, but this remaining gap should be closed before relying on the provider for comprehensive guardrail coverage. Open issues (1)
Fixed/addressed: 1 · PR risk: 5/10 |
apply_guardrail returns the input shape verbatim when the scanned texts are unchanged, so an allowed request logs "allow" rather than "mask" and the success and fail_open paths agree. When a text was sanitized it returns only the remap-relevant keys (texts, images, tools) and drops structured_messages, so a stale unsanitized payload cannot reach the model. Adds allow and mask telemetry assertions.
On the response path (post_call), apply_guardrail now also scans each tool_calls[].function.arguments value against Vigil Guard with source=model_output: ALLOWED leaves the tool call unchanged, BLOCKED raises the guardrail 400, and SANITIZED replaces only that tool call's arguments on a copied tool_calls list without mutating the input. This closes the bypass where disallowed content in model-generated tool-call arguments reached the caller unscanned. Static request tool schemas remain out of scope, and backend failures during tool-call scanning follow the existing fail_closed / fail_open behavior.
…emap The OpenAI chat post-call remap re-used the original tool_calls instead of the tool_calls returned by the guardrail, so a guardrail that sanitizes tool-call arguments by returning a new list (rather than mutating in place) had its changes discarded and the unsanitized arguments reached the caller. The remap now uses guardrailed_inputs["tool_calls"] when present and falls back to the original list otherwise. Adds a regression test with a guardrail that returns a copied tool_calls list; the test fails without this change.
|
@greptileai please re-review; all earlier findings are addressed and the head commit is a016452. |
…umented API Read only the documented sanitized-text fields (sanitizedText, then outputText) and drop an extra fallback key that is not part of the Vigil Guard analyze response contract. This removes dead parsing with no behavior change against the real API; block-reason parsing is unchanged.
|
Greptile status update before checking the PR template box: Greptile reviewed earlier commits in this PR and returned confidence scores of 5/5 and 4/5. All Greptile findings from those reviews were addressed in follow-up commits. After those reviews, the PR also received additional hardening commits:
I requested Greptile re-review for the latest head twice, but the bot has not responded yet. Since the PR template requires the checkbox to be completed before the PR can proceed, I am checking it based on the prior Greptile >=4/5 reviews and this explicit status note. Maintainers: please let me know if you prefer me to wait for a fresh Greptile run on the latest head. |
…compatible The OpenAI chat post-call remap fed the guardrail's returned tool_calls value into the positional response remap. A guardrail that returns tool_calls as a non-list (for example a detection-API JSON envelope) or as a list of a different length than the input could crash the remap or write sanitized arguments onto the wrong tool call. The remap now applies the returned tool_calls only when it is a list whose length matches the tool calls sent for scanning, otherwise it falls back to the original list, which is the prior behavior. Adds regression tests for the non-list and length-mismatch cases.
…fallbacks Adds two vigil_guard precedence cases: a whitespace-only blockMessage falls through to decisionReason, and a non-string sanitizedText is skipped in favor of outputText. These pin the response-field fallbacks against silent regressions.
|
Release-safety summary for this PR. Scope and isolation. The provider is opt-in: it runs only when a guardrail with The one change to shared code. To make response tool-call argument scanning effective, the OpenAI chat guardrail-translation handler now applies the guardrail-returned Test strength. 106 scoped unit tests pass (82 for the provider, 24 for the handler). I also ran mutation testing with 32 targeted mutants across the provider logic and the handler change, and the suite kills all of them for a 100 percent kill rate. The killed mutants include decision handling, fail-open and fail-closed behavior, retry and clamping logic, request and response source mapping, the tool-call rewrite, and the new shape-compatibility guard. CI and end-to-end. CI is green on the head commit. The description includes an end-to-end proof on a live proxy against real OpenAI and a real Vigil Guard backend over verified TLS, covering allow, sanitize, and block on both input and output, the backend-failure path, and secret hygiene. |
|
@veria-ai re review? |
* docs: add Vigil Guard guardrail provider page Document the native vigil_guard guardrail (BerriAI/litellm#29339): config shape, VIGIL_GUARD_API_KEY / VIGIL_GUARD_URL env vars, pre_call and post_call modes, allowed and blocked request examples, supported parameters including unreachable_fallback, and support links. Public references only. * docs: note tool-call argument scanning scope for Vigil Guard Clarify that Vigil Guard scans request and response text and, on post-call, also scans model-generated tool_calls[].function.arguments (replacing them when the decision is sanitized); static tool schemas passed in tools are not scanned.


Relevant issues
N/A
Linear ticket
N/A
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
make test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
CI (LiteLLM team)
Branch creation CI run
Link: c55a8a4a1c
CI run for the last commit
Link: a4288e9
Merge / cherry-pick CI run
Links: N/A (runs after merge)
Screenshots / Proof of Fix
Local checks:
End-to-end proxy proof on a live proxy at
127.0.0.1:4000, calling real OpenAI (gpt-4o-mini) and a real Vigil Guard backend. The proxy reaches the configured Vigil Guard endpoint over TLS that is verified against the Vigil Guard CA in the trust store; TLS verification is not disabled. Guardrail config:HTTP 200; assistant content:The capital of Poland is Warsaw.HTTP 400; message:This message was blocked by Vigil Guard security policy.The request never reached OpenAI.HTTP 200; model output:Mon email est [EMAIL] et mon numero de telephone est [PHONE].The email and phone were redacted before the model saw them.The input passed pre_call; the model response was scanned on source
model_outputand returnedHTTP 400. Confirms response-time scanning and therequest -> user_input/response -> model_outputsource mapping.fail_closed; configured an invalid Vigil Guard key:The analyze call returned
401. The provider logged at ERROR withguardrail_nameandsourceonly (no secret), andfail_closedblocked the request withHTTP 400. Confirms the backend-failure path.AuthorizationBearer header, never in the body, metadata, or logs:Output:
No Vigil Guard key in logs. This matches the unit test asserting the key is absent from the serialized request body.Type
New Feature
Test
Changes
This PR adds a native Vigil Guard guardrail provider for LiteLLM proxy guardrails.
The provider integrates with the existing LiteLLM guardrail hook flow and supports both request-time scanning (
pre_call, mapped to sourceuser_input) and response-time scanning (post_call, mapped to sourcemodel_output). It handles allowed, sanitized, and blocked decisions, maps blocked decisions into the standard LiteLLM guardrail exception path, and preserves configurable fail-open or fail-closed behavior for backend and transport failures, defaulting to fail closed and set viaunreachable_fallbackin YAML.On the response path the provider also scans model-generated tool-call arguments (
tool_calls[].function.arguments) so disallowed content cannot bypass the guardrail through tool calls; a sanitized decision rewrites only that call's arguments and a blocked decision blocks the response. Making that effective also required a fix in the OpenAI chat guardrail-translation handler, which previously re-used the original tool calls and discarded the ones returned by a guardrail. The post-call remap now applies the guardrail-returnedtool_callsonly when it is a list whose length matches the tool calls sent for scanning, and otherwise falls back to the original list. That keeps the shared path safe for guardrails that return a non-list or differently shapedtool_callsvalue, so the change cannot crash or misalign their responses.The implementation also adds the provider configuration model, registers the provider integration type, and includes focused tests for the provider contract, metadata handling, backend failure modes, fail-open and fail-closed behavior, guardrail decision handling, tool-call argument scanning, and the handler remap regression.
Documentation changes are intentionally not included in this code PR. Public documentation will be handled separately in
BerriAI/litellm-docsusing only public Vigil Guard references, primarily https://www.vigilguard.ai.