SDK Mode
Scan content directly from your application code - no LLM proxy required.
Overview
SDK mode exposes a standalone content-checking endpoint (POST /sdk/v1/check) that you can call at any point in your pipeline. Instead of routing LLM traffic through the Quilr gateway, you call this endpoint yourself to scan messages or text for sensitive data and adversarial inputs.
Want to test a key before wiring it into your app? Open the LLM Gateway Playground and switch to the Guardrail check surface.
Common uses:
- Check user input before forwarding to an LLM
- Scan LLM responses before returning them to users
- Scan file uploads, form fields, or other non-LLM content
- Integrate with a self-hosted LiteLLM proxy
Authentication
SDK mode requires a dedicated SDK key - regular LLM proxy keys are rejected with 403.
When creating an API key in the dashboard, set the provider to quilr_sdk. Then use it as a Bearer token:
Authorization: Bearer sk-quilr-xxx
Api-Key: sk-quilr-xxx is also accepted.
You can optionally include an X-User-Email header for identity-aware enforcement if that is configured on your key.
Request Format
POST /sdk/v1/check
Two input formats are supported:
Messages (conversation)
Use this to check a full conversation. The type field is optional.
{
"messages": [
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."}
],
"type": "request",
"metadata": {}
}
Text (raw string)
Use this to check a single piece of text. The type field is optional.
{
"text": "some text to check",
"type": "request"
}
type is "request" or "response".
Response
The endpoint always returns HTTP 200. The response shape depends on which input format you used.
Use action for application control flow, and use predictions to inspect what
the guardrail found.
Messages response
{
"status": "safe | redacted | blocked",
"action": "allow | redact | block",
"messages": [...],
"blocked_text": "...",
"predictions": [
{
"id": "...",
"name": "...",
"type": "redact",
"sensitive_entities": ["123-45-6789"],
"entity_texts_with_subcategories": {
"123-45-6789": "SOCIAL SECURITY NUMBER"
}
}
],
"categories_detected": ["pii", "email", "ssn"],
"placeholder_masking": {
"text": "My SSN is <qe-01a54629efb95228>.",
"messages": [
{ "role": "user", "content": "My SSN is <qe-01a54629efb95228>." }
],
"placeholders": [
{
"placeholder": "<qe-01a54629efb95228>",
"value": "123-45-6789",
"sub_category": "SOCIAL SECURITY NUMBER",
"category_id": "data_risk_category_pii",
"action": "redact",
"message_index": 0
}
]
},
"error": {...}
}
messages- the (possibly redacted) messages array;nullif blockedblocked_text- only present whenstatusisblockedpredictions- rule-level details, including exactsensitive_entitiesandentity_texts_with_subcategorieserror- only present whenstatusisblocked
Text response
{
"status": "safe | redacted | blocked",
"action": "allow | redact | block",
"original_text": "...",
"processed_text": "...",
"predictions": [
{
"id": "...",
"name": "...",
"type": "redact",
"sensitive_entities": ["555-867-5309"],
"entity_texts_with_subcategories": {
"555-867-5309": "PHONE NUMBER"
}
}
],
"categories_detected": ["pii", "phone"],
"placeholder_masking": {
"text": "Call me at <qe-59c0b4a6fc3c3b2c>.",
"messages": null,
"placeholders": [
{
"placeholder": "<qe-59c0b4a6fc3c3b2c>",
"value": "555-867-5309",
"sub_category": "PHONE NUMBER",
"category_id": "data_risk_category_pii",
"action": "redact"
}
]
},
"error": {...}
}
processed_text- the redacted text;nullif blockedpredictions- rule-level details, including exactsensitive_entitiesandentity_texts_with_subcategorieserror- only present whenstatusisblocked
Placeholder masking
Every response also includes placeholder_masking. This is an additive view of
the same content where sensitive values are replaced with hash-based placeholders
such as <qe-358100c210df061d>, <qe-4c658021550ddeb2>, or
<qe-f52fbd32b2b3b86f>.
Use this when your application needs visually distinct, reversible placeholders
instead of same-length X redaction. The placeholder token uses the format
<qe-{16-char-hash}> and stays stable for the exact matched source value;
placeholders[] maps each token back to the original value and detection
metadata. Message checks include placeholder_masking.messages; raw text checks
set placeholder_masking.messages to null.
Code Examples
Python - httpx (async)
A typical pattern: check the user message before sending it to your LLM, then check the LLM response before returning it to the user.
import httpx
QUILR_BASE = "https://guardrails-usa-2.quilr.ai"
QUILR_SDK_KEY = "sk-quilr-xxx"
async def check_messages(messages: list[dict]) -> dict:
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{QUILR_BASE}/sdk/v1/check",
headers={"Authorization": f"Bearer {QUILR_SDK_KEY}"},
json={"messages": messages, "type": "request"},
timeout=5,
)
resp.raise_for_status()
return resp.json()
async def check_text(text: str, type_: str = "response") -> dict:
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{QUILR_BASE}/sdk/v1/check",
headers={"Authorization": f"Bearer {QUILR_SDK_KEY}"},
json={"text": text, "type": type_},
timeout=5,
)
resp.raise_for_status()
return resp.json()
# --- Usage ---
import asyncio
from openai import AsyncOpenAI
openai = AsyncOpenAI(api_key="sk-openai-xxx")
async def safe_chat(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
# 1. Check input
result = await check_messages(messages)
if result["status"] == "blocked":
raise ValueError(f"Input blocked: {result['categories_detected']}")
if result["status"] == "redacted":
messages = result["messages"] # use redacted version
# 2. Call LLM
response = await openai.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
)
reply = response.choices[0].message.content
# 3. Check output
result = await check_text(reply, type_="response")
if result["status"] == "blocked":
raise ValueError(f"Response blocked: {result['categories_detected']}")
if result["status"] == "redacted":
reply = result["processed_text"]
return reply
asyncio.run(safe_chat("What is my SSN?"))
Python - requests (sync)
import requests
QUILR_BASE = "https://guardrails-usa-2.quilr.ai"
QUILR_SDK_KEY = "sk-quilr-xxx"
def check_text(text: str, type_: str = "response") -> dict:
resp = requests.post(
f"{QUILR_BASE}/sdk/v1/check",
headers={"Authorization": f"Bearer {QUILR_SDK_KEY}"},
json={"text": text, "type": type_},
timeout=5,
)
resp.raise_for_status()
return resp.json()
# Check a piece of text before storing or displaying it
result = check_text("My credit card is 4111 1111 1111 1111", type_="request")
match result["status"]:
case "safe":
print("No issues found")
case "redacted":
print("Cleaned text:", result["processed_text"])
case "blocked":
print("Blocked. Detected:", result["categories_detected"])
JavaScript / TypeScript - fetch
const QUILR_BASE = "https://guardrails-usa-2.quilr.ai";
const QUILR_SDK_KEY = "sk-quilr-xxx";
async function checkMessages(messages: Array<{ role: string; content: string }>) {
const res = await fetch(`${QUILR_BASE}/sdk/v1/check`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${QUILR_SDK_KEY}`,
},
body: JSON.stringify({ messages, type: "request" }),
signal: AbortSignal.timeout(5000),
});
if (!res.ok) throw new Error(`Quilr error: ${res.status}`);
return res.json();
}
async function checkText(text: string, type: "request" | "response" = "response") {
const res = await fetch(`${QUILR_BASE}/sdk/v1/check`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${QUILR_SDK_KEY}`,
},
body: JSON.stringify({ text, type }),
signal: AbortSignal.timeout(5000),
});
if (!res.ok) throw new Error(`Quilr error: ${res.status}`);
return res.json();
}
// Example: guard a chat endpoint
async function safeChat(userMessage: string): Promise<string> {
const messages = [{ role: "user", content: userMessage }];
const inputResult = await checkMessages(messages);
if (inputResult.status === "blocked") {
throw new Error(`Blocked: ${inputResult.categories_detected.join(", ")}`);
}
const checkedMessages =
inputResult.status === "redacted" ? inputResult.messages : messages;
// ... call your LLM with checkedMessages ...
const llmReply = "...";
const outputResult = await checkText(llmReply, "response");
if (outputResult.status === "blocked") {
throw new Error(`Response blocked: ${outputResult.categories_detected.join(", ")}`);
}
return outputResult.status === "redacted" ? outputResult.processed_text : llmReply;
}
cURL
# Check raw text
curl -X POST https://guardrails-usa-2.quilr.ai/sdk/v1/check \
-H "Authorization: Bearer sk-quilr-xxx" \
-H "Content-Type: application/json" \
-d '{"text": "Call me at 555-867-5309", "type": "request"}'
# Check a conversation
curl -X POST https://guardrails-usa-2.quilr.ai/sdk/v1/check \
-H "Authorization: Bearer sk-quilr-xxx" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "What is the password for admin@acme.com?"}
],
"type": "request"
}'
LiteLLM Guardrails Integration
If you run a self-hosted LiteLLM proxy, you can plug Quilr guardrails in as a native guardrail plugin. The plugin calls /sdk/v1/check automatically on every request and/or response - no changes needed in your application code.
Prerequisites
- A Quilr guardrails API key, from either Quilr-hosted guardrails or a self-hosted Quilr guardrails deployment
- Download quilr_litellm_guardrails.py and place it in the same directory as your LiteLLM
config.yaml
LiteLLM resolves custom guardrails by module path relative to the proxy working directory, so the plugin ships as a single file you drop next to config.yaml. There is no package to install.
Environment variables
QUILR_GUARDRAILS_KEY=sk-quilr-XXXXXXXXX
QUILR_GUARDRAILS_BASE_URL=<QUILR GUARDRAILS BASE URL>
QUILR_GUARDRAILS_TIMEOUT=3 # Optional: timeout in seconds (default: 3). On timeout or API error, the request passes through.
Choosing a mode
Add guardrails to your LiteLLM config.yaml using the modes you need:
When to use during_call vs pre_call:
- Use
during_callfor better latency, since the guardrail runs concurrently with the LLM - Use
pre_callif you want to avoid wasting LLM compute on blocked requests
LiteLLM config.yaml
Input guardrail only (pre_call)
guardrails:
- guardrail_name: "quilr-input"
litellm_params:
guardrail: quilr_litellm_guardrails.QuilrGuardrail
mode: "pre_call"
default_on: true
Input guardrail with lower latency (during_call)
guardrails:
- guardrail_name: "quilr-input-duringcall"
litellm_params:
guardrail: quilr_litellm_guardrails.QuilrGuardrail
mode: "during_call"
default_on: true
Output guardrail only (post_call)
guardrails:
- guardrail_name: "quilr-output"
litellm_params:
guardrail: quilr_litellm_guardrails.QuilrGuardrail
mode: "post_call"
default_on: true
Both input and output guardrails
guardrails:
- guardrail_name: "quilr-input"
litellm_params:
guardrail: quilr_litellm_guardrails.QuilrGuardrail
mode: "pre_call"
default_on: true
- guardrail_name: "quilr-output"
litellm_params:
guardrail: quilr_litellm_guardrails.QuilrGuardrail
mode: "post_call"
default_on: true
Optional filtering
You can limit which requests have guardrails applied using these environment variables:
# Only apply guardrails to specific models (comma-separated)
APPLY_QUILR_GUARDRAILS_FOR_MODELS=gpt-4,gpt-4o,claude-3-opus
# Only apply guardrails to specific API key names (comma-separated)
APPLY_QUILR_GUARDRAILS_FOR_KEY_NAMES=production-key,user-facing-key
If neither variable is set, guardrails apply to all requests. If both are set, a request must match both filters (AND logic) for guardrails to be applied.
Enabling guardrails per request
With default_on: true, guardrails run on every request. If you set default_on: false, pass the guardrail names in the request body instead:
curl -X POST http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer sk-litellm-xxx" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Hello"}],
"guardrails": ["quilr-input-duringcall", "quilr-output"]
}'
Behavior summary
On timeout or any unexpected error from the Quilr API, the request passes through unchanged.