Adding Cycraft XecGuard integration - #26011
Conversation
There was a problem hiding this comment.
Low: Raw third-party API response exposed to end users
This PR adds a well-structured XecGuard guardrail integration. The only security observation is that the full raw XecGuard API response is included in the HTTPException detail returned to end users, which could expose internal scan metadata beyond what's in the formatted error message.
| detail={ | ||
| "error": self._format_scan_block_message(scan_result), | ||
| "guardrail_name": self.guardrail_name or "xecguard", | ||
| "xecguard_response": scan_result, |
There was a problem hiding this comment.
Low: Raw API response forwarded to end user
The entire scan_result dict from the XecGuard API is included in the HTTPException detail, which FastAPI serializes back to the client. While the formatted error field already contains a curated subset (policy names, trace_id, truncated rationale), the raw response may include additional fields the XecGuard API adds in the future — internal metadata, scoring details, etc. Consider dropping the xecguard_response key from the user-facing error, or at minimum allowlisting only specific fields (decision, trace_id).
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR integrates CyCraft XecGuard as a first-class partner guardrail in LiteLLM, supporting all four event hooks ( Two minor P2 items were found:
Confidence Score: 5/5Safe to merge — only P2 documentation/dead-code findings remain All blocking concerns from prior review rounds have been resolved. The two remaining findings are both P2: a field description that understates the actual default policy count, and a dead-code guard block that is harmless but confusing. Neither affects runtime correctness or security. litellm/types/proxy/guardrails/guardrail_hooks/xecguard.py (description mismatch) and litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py (dead-code guard)
|
| Filename | Overview |
|---|---|
| litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py | Core guardrail hook: implements all four event modes, grounding validation, and message normalization; contains a harmless but unreachable dead-code guard in async_logging_hook (wrong key path) |
| litellm/types/proxy/guardrails/guardrail_hooks/xecguard.py | Pydantic config model with multiselect policy UI metadata; policy_names description understates the actual 3-policy default |
| litellm/types/guardrails.py | Adds XECGUARD enum entry and XecGuardConfigModel to LitellmParams mixin chain; correctly follows existing integration pattern |
| litellm/proxy/guardrails/guardrail_hooks/xecguard/init.py | Wires XecGuardGuardrail into the initializer and class registries; consistent with other integration patterns |
| tests/test_litellm/proxy/guardrails/guardrail_hooks/test_xecguard.py | 119 unit tests with 100% branch coverage; all HTTP calls are mocked; covers all four event hooks and grounding scenarios |
Sequence Diagram
sequenceDiagram
participant Client
participant LiteLLM as LiteLLM Proxy
participant XG as XecGuard /scan
participant GR as XecGuard /grounding
participant LLM as LLM Provider
Client->>LiteLLM: POST /v1/chat/completions
alt pre_call / during_call
LiteLLM->>XG: POST /xecguard/v1/scan (scan_type=input)
XG-->>LiteLLM: decision SAFE or UNSAFE
alt UNSAFE
LiteLLM-->>Client: 400 Blocked by XecGuard
end
end
LiteLLM->>LLM: Forward request
LLM-->>LiteLLM: Response
alt post_call
LiteLLM->>XG: POST /xecguard/v1/scan (scan_type=response)
XG-->>LiteLLM: decision SAFE or UNSAFE
alt UNSAFE
LiteLLM-->>Client: 400 Blocked by XecGuard
end
opt metadata.xecguard_grounding_documents present
LiteLLM->>GR: POST /xecguard/v1/grounding
GR-->>LiteLLM: decision SAFE or UNSAFE
alt UNSAFE
LiteLLM-->>Client: 400 Blocked by XecGuard grounding
end
end
end
alt logging_only
LiteLLM->>XG: POST /xecguard/v1/scan (suppress_errors=True)
XG-->>LiteLLM: scan result (never blocks)
Note over LiteLLM: guardrail_information to standard_logging_object
end
LiteLLM-->>Client: 200 OK
Reviews (3): Last reviewed commit: "Make sure every mode is logged correctly" | Re-trigger Greptile
| return kwargs, result | ||
|
|
||
| scan_result = await self._call_scan( | ||
| messages=messages, | ||
| scan_type=scan_type, | ||
| suppress_errors=True, | ||
| ) | ||
| if scan_result is None: | ||
| return kwargs, result | ||
|
|
||
| guardrail_status: GuardrailStatus = ( | ||
| "guardrail_intervened" |
There was a problem hiding this comment.
logging_hook silently skips the scan when an event loop is already running
When loop.is_running() is True (which is almost always the case inside an async framework like Starlette/FastAPI), the sync logging_hook immediately returns without scanning and without emitting any log message. This means logging_only mode is silently a no-op for virtually every request that comes through the proxy. If this silent-skip behaviour is intentional (to avoid blocking), a verbose_proxy_logger.debug message would make it observable and prevent confusion during debugging.
There was a problem hiding this comment.
This particular branch isn't actually a silent no-op for proxy requests. LiteLLM dispatches async callsites (which is all proxy traffic) to async_logging_hook, not to this sync logging_hook. Our async_logging_hook override runs the full scan and attaches guardrail_information to the standard logging payload, so logging_only mode works correctly end-to-end for proxy requests.
The sync logging_hook is a defensive fallback for sync callsites (e.g. litellm.completion() invoked outside any event loop). When it's reached from within a running loop, calling loop.run_until_complete() would raise RuntimeError: This event loop is already running. So the early-return is intentional to avoid a crash, not an observability gap.
|
Two out of the three Greptile review comments from the latest round have been addressed. Regarding the remaining P2 review, please refer to our explanation provided directly below that comment. @ishaan-jaff , we would greatly appreciate it if you could review this PR when you have a chance. Thank you. |
|
@clyang — could you add a screenshot or short video showing that this change works as expected? It really helps reviewers verify the integration quickly. Thanks! |
Hi @krrish-berri-2 , The video demonstrates the following:
I look forward to your review and am happy to address any comments you may have. |
692638b
into
BerriAI:litellm_oss_branch
Add XecGuard Guardrail Integration
Pre-Submission Checklist
tests/test_litellm/directory (119 unit tests)make test-unitxecguardmoduleType
🆕 New Feature
📖 Documentation
✅ Test
Changes Summary
Overview
Integrates CyCraft XecGuard as a first-class partner guardrail in LiteLLM's proxy. Provides multi-policy prompt/response scanning (prompt injection, harmful content, PII, system-prompt enforcement, bias, skills protection) and RAG context-grounding validation via the dedicated
/groundingendpoint.Architecture
Backend Components
XECGUARDenum entry inSupportedGuardrailIntegrationsXecGuardGuardrailclass implementingCustomGuardrailwithapply_guardrailvia POST to/xecguard/v1/scanusingAuthorization: Bearer <xgs_*>pre_call,during_call,post_call, andlogging_onlyapply_guardrailhandles pre/during/post via the framework's unified dispatchasync_logging_hook+ synclogging_hookoverride for observe-only scans inlogging_onlymode — never blocks, swallows all errors, emits a guardrail_information entry ontostandard_logging_objectfor downstream loggers (Langfuse, DataDog, OTEL)/xecguard/v1/grounding— automatically invoked onpost_callwhenmetadata.xecguard_grounding_documents: [{document_id, context}, ...]is present in the request; detects CONFLICT / BASELESS / INCOMPLETE violations; configurablegrounding_strictness(BALANCED/STRICT)api_key,api_base,xecguard_model,policy_names(list),block_on_error(fail-closed by default),grounding_strictnessapi_key,api_base,xecguard_model,policy_names,block_on_error,grounding_strictness,ui_friendly_name()Frontend Components
Security,Policy,Grounding,RAG), and logoguardrailLogoMapguardrail_provider_mappolicy_namessurfaces as a multi-select picker exposing all six default XecGuard policies (SystemPromptEnforcement,GeneralPromptAttackProtection,ContentBiasProtection,HarmfulContentProtection,SkillsProtection,PIISensitiveDataProtection) instead of a free-form list inputDocumentation
docs/my-website/docs/proxy/guardrails/xecguard.mdwith quick start, all four event-hook modes explained, the six available policies table, context-grounding usage with request-time metadata, input+output pipeline example, fail-open and logging-only examples, and the full-history guaranteeproxy/guardrails/Testing
pytest --cov=litellm.proxy.guardrails.guardrail_hooks.xecguard --cov=litellm.types.proxy.guardrails.guardrail_hooks.xecguard --cov-branch).Files Modified/Created
litellm/types/guardrails.pyLitellmParamsmixinlitellm/types/proxy/guardrails/guardrail_hooks/xecguard.pylitellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.pylitellm/proxy/guardrails/guardrail_hooks/xecguard/__init__.pytests/test_litellm/proxy/guardrails/guardrail_hooks/test_xecguard.pyui/litellm-dashboard/public/assets/logos/xecguard.svgui/litellm-dashboard/src/components/guardrails/guardrail_garden_data.tsui/litellm-dashboard/src/components/guardrails/guardrail_garden_configs.tsui/litellm-dashboard/src/components/guardrails/guardrail_info_helpers.tsxdocs/my-website/docs/proxy/guardrails/xecguard.mddocs/my-website/sidebars.jsTest Results
xecguardmodule: ✅Usage Example
config.yaml:Per-request (for RAG grounding):