Microsoft Purview Guardrail
LiteLLM supports Microsoft Purview DLP policies via the Microsoft Graph processContent API.
Supported modes
| Mode | What it does |
|---|---|
pre_call | Evaluates the user prompt against DLP policies before the LLM call. Blocks if a restrictAccess/block policy action fires. |
post_call | Evaluates the LLM response against DLP policies. Blocks if a restrictAccess/block policy action fires. |
logging_only | Sends both prompt and response to Purview for audit. Never blocks the request. |
Prerequisites
-
An Entra app registration with the following Microsoft Graph application permissions:
InformationProtectionPolicy.Read.AllProtectionScopes.Compute.UserContent.Process.User
-
A DLP policy in Microsoft Purview targeting your app registration's
client_idas a Protected App. Without an active policy,policyActionsin the API response will always be empty. -
Entra user object IDs — the LiteLLM API key (or JWT) used for the request must have a
user_idbound to it that is the Entra object ID of the end-user (not a username or email). Blocking hooks (pre_call/post_call) reject the request with HTTP 400 when the key has no bound user ID; the logging-only hook skips the audit instead.
Quick Start
1. Register your app in Entra
# Create app registration and note the appId (client_id) and tenantId
az ad app create --display-name "LiteLLM-Purview"
az ad sp create --id <appId>
# Create a client secret
az ad app credential reset --id <appId> --append
Grant the permissions listed above in the Azure portal under App registrations → API permissions, then Grant admin consent.
2. Define the guardrail in config.yaml
- pre_call
- post_call
- logging_only (audit)
model_list:
- model_name: gpt-5.6-terra
litellm_params:
model: openai/gpt-5.6-terra
api_key: os.environ/OPENAI_API_KEY
guardrails:
- guardrail_name: purview-prompt-dlp
litellm_params:
guardrail: microsoft_purview
mode: pre_call
api_key: os.environ/AZURE_CLIENT_SECRET # client_secret
tenant_id: os.environ/AZURE_TENANT_ID
client_id: os.environ/AZURE_CLIENT_ID
default_on: true
guardrails:
- guardrail_name: purview-response-dlp
litellm_params:
guardrail: microsoft_purview
mode: post_call
api_key: os.environ/AZURE_CLIENT_SECRET
tenant_id: os.environ/AZURE_TENANT_ID
client_id: os.environ/AZURE_CLIENT_ID
default_on: true
guardrails:
- guardrail_name: purview-audit
litellm_params:
guardrail: microsoft_purview
mode: logging_only
api_key: os.environ/AZURE_CLIENT_SECRET
tenant_id: os.environ/AZURE_TENANT_ID
client_id: os.environ/AZURE_CLIENT_ID
default_on: true
3. Start LiteLLM Gateway
litellm --config config.yaml --detailed_debug
4. Test request
Send the request with a LiteLLM key whose bound user_id is the Entra object ID of the end-user (see User ID resolution). metadata.user_id is shown for reference but is not used to pick the Purview identity:
curl -X POST http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $LITELLM_API_KEY" \
-d '{
"model": "gpt-5.6-terra",
"messages": [{"role": "user", "content": "Hello, what is the capital of France?"}],
"metadata": {"user_id": "<entra-user-object-id>"}
}'
When a DLP policy blocks the request, LiteLLM returns:
{
"error": {
"status_code": 400,
"message": {
"error": "Microsoft Purview DLP: Content blocked by policy",
"activity": "uploadText"
}
}
}
Supported Params
| Param | Type | Required | Description |
|---|---|---|---|
guardrail | str | Yes | Must be "microsoft_purview" |
mode | str | Yes | pre_call, post_call, or logging_only |
api_key | str | Yes | Entra app client secret (can use os.environ/VAR) |
tenant_id | str | Yes | Entra tenant ID |
client_id | str | Yes | Entra app registration client ID (also used as the Protected App identifier in Purview) |
default_on | bool | No | Run this guardrail for every request. Default: false |
purview_app_name | str | No | App name reported to Purview in processContent. Default: "LiteLLM" |
user_id_field | str | No | Caller-supplied metadata field. Not used as the identity sent to Purview; only detected to return a more specific error message (see User ID resolution). Default: "user_id" |
User ID resolution
The Entra object ID used for Purview protectionScopes / processContent comes only from user_api_key_dict.user_id, the user bound to the LiteLLM API key or JWT. Caller-influenceable sources (user_api_key_dict.end_user_id, metadata["user_api_key_user_id"], and metadata[user_id_field]) are intentionally ignored so a client cannot run the DLP check under another Entra user's policy.
If the key has no bound user_id, the blocking hooks (pre_call / post_call) fail closed and reject the request with HTTP 400. When only caller-supplied identity fields are present, the error message notes that caller-supplied metadata cannot be used for blocking DLP.
The logging-only hook reads the proxy-injected user_api_key_user_id from litellm_params.metadata (populated from the same key-bound user_id). If it is absent, the audit is skipped rather than misattributed.
Enabling per request
When default_on: false, you can opt individual requests in or out:
curl -X POST http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $LITELLM_API_KEY" \
-d '{
"model": "gpt-5.6-terra",
"messages": [{"role": "user", "content": "Hello"}],
"guardrails": ["purview-prompt-dlp"],
"metadata": {"user_id": "<entra-user-object-id>"}
}'
How it works
-
Token acquisition — The guardrail uses the OAuth2 client credentials grant to get a Microsoft Graph bearer token. Tokens are cached until 60 seconds before expiry.
-
Protection scope computation — Before each DLP check, the guardrail calls
protectionScopes/computefor the user to retrieve the ETag representing current policy state. Results are cached per user for 1 hour (per Microsoft's recommendation). If theprocessContentresponse indicates policies have changed (protectionScopeState: modified), the cache is invalidated. -
Content evaluation — The guardrail calls
processContentwith the text and anactivityMetadata.activityofuploadText(prompts) ordownloadText(responses). Chat (/v1/chat/completions): pre-call concatenates string content from every message (all roles); post-call uses assistantmessage.contentfrom every chat choice whenn > 1. Legacy text completions (/v1/completions): pre-call uses thepromptfield (string or list of strings; token-id-only prompts are skipped); post-call usestextfrom everyTextChoicesentry. -
Block decision — If any
policyActionsentry has@odata.typecontainingrestrictAccessActionandrestrictionAction: "block", the guardrail raises an HTTP 400. -
Audit logging — In all modes, guardrail results are recorded in
metadata.standard_logging_guardrail_informationand flow to configured observability backends (Langfuse, Datadog, OTEL, etc.).