mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 04:21:34 +00:00
[Feat] UI - allow adding pass through guardrails through UI (#17226)
* add PassThroughGuardrailsConfig * init JsonPathExtractor * feat PassthroughGuardrailHandler * feat pt guardrails * pt guardrails * add Pass-Through Endpoint Guardrail Translation * add PassThroughEndpointHandler * execute simple guardrail config and dict settings * TestPassthroughGuardrailHandlerNormalizeConfig * add passthrough_guardrails_config on litellm logging obj * add LiteLLMLoggingObj to base trasaltino * cleaner _get_guardrail_settings * update guardrails settings * docs pt guardrail * docs Guardrails on Pass-Through Endpoints * fix typing * fix typing * test_no_fields_set_sends_full_body * fix typing * init add pass through guardrails * ui allow setting target fields on gd * docs ui settings guardrails
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# Guardrails on Pass-Through Endpoints
|
||||
|
||||
import Image from '@theme/IdealImage';
|
||||
|
||||
## Overview
|
||||
|
||||
| Property | Details |
|
||||
@@ -10,7 +12,41 @@
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Define guardrails and pass-through endpoint
|
||||
You can configure guardrails on pass-through endpoints either via the **UI** (recommended) or **config file**.
|
||||
|
||||
### Using the UI
|
||||
|
||||
#### 1. Navigate to Pass-Through Endpoints
|
||||
|
||||
Go to **Models + Endpoints** → Click **+ Add Pass-Through Endpoint**
|
||||
|
||||
<Image img={require('../../img/pt_guard1.png')} alt="Add guardrails to pass-through endpoint" />
|
||||
|
||||
Scroll to the **Guardrails** section and select which guardrails to enforce.
|
||||
|
||||
:::tip Default Behavior
|
||||
By default, you don't need to specify fields - LiteLLM will JSON dump the entire request/response payload and send it to the guardrail.
|
||||
:::
|
||||
|
||||
#### 2. Target Specific Fields (Optional)
|
||||
|
||||
<Image img={require('../../img/pt_guard2.png')} alt="Configure field-level targeting" />
|
||||
|
||||
To check only specific fields instead of the entire payload:
|
||||
|
||||
1. Select your guardrails
|
||||
2. In **Field Targeting (Optional)**, specify fields for each guardrail
|
||||
3. Use the quick-add buttons (`+ query`, `+ documents[*]`) or type custom JSONPath expressions
|
||||
4. **Request Fields (pre_call)**: Fields to check before sending to target API
|
||||
5. **Response Fields (post_call)**: Fields to check in the response from target API
|
||||
|
||||
**Example**: In the screenshot above, we set `query` as a request field, so only the `query` field is sent to the guardrail instead of the entire request.
|
||||
|
||||
---
|
||||
|
||||
### Using Config File
|
||||
|
||||
#### 1. Define guardrails and pass-through endpoint
|
||||
|
||||
```yaml showLineNumbers title="config.yaml"
|
||||
guardrails:
|
||||
@@ -31,13 +67,13 @@ general_settings:
|
||||
pii-guard:
|
||||
```
|
||||
|
||||
### 2. Start proxy
|
||||
#### 2. Start proxy
|
||||
|
||||
```bash
|
||||
litellm --config config.yaml
|
||||
```
|
||||
|
||||
### 3. Test request
|
||||
#### 3. Test request
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:4000/v1/rerank" \
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 769 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 548 KiB |
@@ -261,7 +261,7 @@ class PassthroughGuardrailHandler:
|
||||
for guardrail_name in normalized_config.keys():
|
||||
guardrails_to_run[guardrail_name] = True
|
||||
verbose_proxy_logger.debug(
|
||||
"Added passthrough-specific guardrail"
|
||||
"Added passthrough-specific guardrail: %s", guardrail_name
|
||||
)
|
||||
|
||||
# Add org/team/key level guardrails using shared helper
|
||||
@@ -279,12 +279,12 @@ class PassthroughGuardrailHandler:
|
||||
if guardrail_name not in guardrails_to_run:
|
||||
guardrails_to_run[guardrail_name] = True
|
||||
verbose_proxy_logger.debug(
|
||||
"Added inherited guardrail (key/team level)"
|
||||
"Added inherited guardrail (key/team level): %s", guardrail_name
|
||||
)
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
"Collected total guardrails for passthrough endpoint: %d",
|
||||
len(guardrails_to_run),
|
||||
"Collected guardrails for passthrough endpoint: %s",
|
||||
list(guardrails_to_run.keys()),
|
||||
)
|
||||
|
||||
return guardrails_to_run if guardrails_to_run else None
|
||||
|
||||
@@ -27,6 +27,7 @@ import { passThroughItem } from "./pass_through_settings";
|
||||
import RoutePreview from "./route_preview";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import PassThroughSecuritySection from "./common_components/PassThroughSecuritySection";
|
||||
import PassThroughGuardrailsSection from "./common_components/PassThroughGuardrailsSection";
|
||||
const { Option } = Select2;
|
||||
|
||||
interface AddFallbacksProps {
|
||||
@@ -51,11 +52,13 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
||||
const [targetValue, setTargetValue] = useState("");
|
||||
const [includeSubpath, setIncludeSubpath] = useState(true);
|
||||
const [authEnabled, setAuthEnabled] = useState(false);
|
||||
const [guardrails, setGuardrails] = useState<Record<string, { request_fields?: string[]; response_fields?: string[] } | null>>({});
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
setPathValue("");
|
||||
setTargetValue("");
|
||||
setIncludeSubpath(true);
|
||||
setGuardrails({});
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
@@ -77,6 +80,12 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
||||
if (!premiumUser && 'auth' in formValues) {
|
||||
delete formValues.auth;
|
||||
}
|
||||
|
||||
// Add guardrails to formValues (only if not empty)
|
||||
if (guardrails && Object.keys(guardrails).length > 0) {
|
||||
formValues.guardrails = guardrails;
|
||||
}
|
||||
|
||||
console.log(`formValues: ${JSON.stringify(formValues)}`);
|
||||
|
||||
const response = await createPassThroughEndpoint(accessToken, formValues);
|
||||
@@ -92,6 +101,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
||||
setPathValue("");
|
||||
setTargetValue("");
|
||||
setIncludeSubpath(true);
|
||||
setGuardrails({});
|
||||
setIsModalVisible(false);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Error creating pass-through endpoint: " + error);
|
||||
@@ -249,6 +259,14 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
||||
form.setFieldsValue({ auth: checked });
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Guardrails Section */}
|
||||
<PassThroughGuardrailsSection
|
||||
accessToken={accessToken}
|
||||
value={guardrails}
|
||||
onChange={setGuardrails}
|
||||
/>
|
||||
|
||||
{/* Billing Section */}
|
||||
<Card className="p-6">
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">Billing</Title>
|
||||
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, Title, Subtitle } from "@tremor/react";
|
||||
import { Form, Input, Select, Tooltip, Alert } from "antd";
|
||||
import { InfoCircleOutlined, PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import GuardrailSelector from "../guardrails/GuardrailSelector";
|
||||
|
||||
interface PassThroughGuardrailsSectionProps {
|
||||
accessToken: string;
|
||||
value?: Record<string, { request_fields?: string[]; response_fields?: string[] } | null>;
|
||||
onChange?: (guardrails: Record<string, { request_fields?: string[]; response_fields?: string[] } | null>) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps> = ({
|
||||
accessToken,
|
||||
value = {},
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [selectedGuardrails, setSelectedGuardrails] = useState<string[]>(Object.keys(value));
|
||||
const [guardrailSettings, setGuardrailSettings] = useState<
|
||||
Record<string, { request_fields?: string[]; response_fields?: string[] } | null>
|
||||
>(value);
|
||||
|
||||
// Sync external value changes
|
||||
useEffect(() => {
|
||||
setGuardrailSettings(value);
|
||||
setSelectedGuardrails(Object.keys(value));
|
||||
}, [value]);
|
||||
|
||||
const handleGuardrailChange = (guardrails: string[]) => {
|
||||
setSelectedGuardrails(guardrails);
|
||||
|
||||
// Create new settings object with selected guardrails
|
||||
const newSettings: Record<string, { request_fields?: string[]; response_fields?: string[] } | null> = {};
|
||||
guardrails.forEach((name) => {
|
||||
// Preserve existing settings or set to null (uses entire payload)
|
||||
newSettings[name] = guardrailSettings[name] || null;
|
||||
});
|
||||
|
||||
setGuardrailSettings(newSettings);
|
||||
if (onChange) {
|
||||
onChange(newSettings);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFieldChange = (
|
||||
guardrailName: string,
|
||||
fieldType: "request_fields" | "response_fields",
|
||||
fields: string[]
|
||||
) => {
|
||||
const currentSettings = guardrailSettings[guardrailName] || {};
|
||||
const newSettings = {
|
||||
...guardrailSettings,
|
||||
[guardrailName]: {
|
||||
...currentSettings,
|
||||
[fieldType]: fields.length > 0 ? fields : undefined,
|
||||
},
|
||||
};
|
||||
|
||||
// If no fields are set, set to null (entire payload)
|
||||
if (!newSettings[guardrailName]?.request_fields && !newSettings[guardrailName]?.response_fields) {
|
||||
newSettings[guardrailName] = null;
|
||||
}
|
||||
|
||||
setGuardrailSettings(newSettings);
|
||||
if (onChange) {
|
||||
onChange(newSettings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<Title className="text-lg font-semibold text-gray-900 mb-2">Guardrails</Title>
|
||||
<Subtitle className="text-gray-600 mb-6">
|
||||
Configure guardrails to enforce policies on requests and responses. Guardrails are opt-in for passthrough
|
||||
endpoints.
|
||||
</Subtitle>
|
||||
|
||||
<Alert
|
||||
message={
|
||||
<span>
|
||||
Field-Level Targeting{" "}
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/pass_through_guardrails#field-level-targeting"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 underline"
|
||||
>
|
||||
(Learn More)
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
Optionally specify which fields to check. If left empty, the entire request/response is sent to the guardrail.
|
||||
</div>
|
||||
<div className="text-xs space-y-1 mt-2">
|
||||
<div className="font-medium">Common Examples:</div>
|
||||
<div>• <code className="bg-gray-100 px-1 rounded">query</code> - Single field</div>
|
||||
<div>• <code className="bg-gray-100 px-1 rounded">documents[*].text</code> - All text in documents array</div>
|
||||
<div>• <code className="bg-gray-100 px-1 rounded">messages[*].content</code> - All message contents</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Select Guardrails
|
||||
<Tooltip title="Choose which guardrails should run on this endpoint. Org/team/key level guardrails will also be included.">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<GuardrailSelector
|
||||
accessToken={accessToken}
|
||||
value={selectedGuardrails}
|
||||
onChange={handleGuardrailChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{selectedGuardrails.length > 0 && (
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-sm font-medium text-gray-700">Field Targeting (Optional)</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
💡 Tip: Leave empty to check entire payload
|
||||
</div>
|
||||
</div>
|
||||
{selectedGuardrails.map((guardrailName) => (
|
||||
<Card key={guardrailName} className="p-4 bg-gray-50">
|
||||
<div className="text-sm font-medium text-gray-900 mb-3">{guardrailName}</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-xs text-gray-600 flex items-center">
|
||||
Request Fields (pre_call)
|
||||
<Tooltip title={
|
||||
<div>
|
||||
<div className="font-medium mb-1">Specify which request fields to check</div>
|
||||
<div className="text-xs space-y-1">
|
||||
<div>Examples:</div>
|
||||
<div>• query</div>
|
||||
<div>• documents[*].text</div>
|
||||
<div>• messages[*].content</div>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<InfoCircleOutlined className="ml-1 text-gray-400" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const current = guardrailSettings[guardrailName]?.request_fields || [];
|
||||
handleFieldChange(guardrailName, "request_fields", [...current, "query"]);
|
||||
}}
|
||||
className="text-xs px-2 py-1 bg-white border border-gray-300 rounded hover:bg-gray-50"
|
||||
disabled={disabled}
|
||||
>
|
||||
+ query
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const current = guardrailSettings[guardrailName]?.request_fields || [];
|
||||
handleFieldChange(guardrailName, "request_fields", [...current, "documents[*]"]);
|
||||
}}
|
||||
className="text-xs px-2 py-1 bg-white border border-gray-300 rounded hover:bg-gray-50"
|
||||
disabled={disabled}
|
||||
>
|
||||
+ documents[*]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Type field name or use + buttons above (e.g., query, documents[*].text)"
|
||||
value={guardrailSettings[guardrailName]?.request_fields || []}
|
||||
onChange={(fields) => handleFieldChange(guardrailName, "request_fields", fields)}
|
||||
disabled={disabled}
|
||||
tokenSeparators={[","]}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-xs text-gray-600 flex items-center">
|
||||
Response Fields (post_call)
|
||||
<Tooltip title={
|
||||
<div>
|
||||
<div className="font-medium mb-1">Specify which response fields to check</div>
|
||||
<div className="text-xs space-y-1">
|
||||
<div>Examples:</div>
|
||||
<div>• results[*].text</div>
|
||||
<div>• choices[*].message.content</div>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<InfoCircleOutlined className="ml-1 text-gray-400" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const current = guardrailSettings[guardrailName]?.response_fields || [];
|
||||
handleFieldChange(guardrailName, "response_fields", [...current, "results[*]"]);
|
||||
}}
|
||||
className="text-xs px-2 py-1 bg-white border border-gray-300 rounded hover:bg-gray-50"
|
||||
disabled={disabled}
|
||||
>
|
||||
+ results[*]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Type field name or use + buttons above (e.g., results[*].text)"
|
||||
value={guardrailSettings[guardrailName]?.response_fields || []}
|
||||
onChange={(fields) => handleFieldChange(guardrailName, "response_fields", fields)}
|
||||
disabled={disabled}
|
||||
tokenSeparators={[","]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PassThroughGuardrailsSection;
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Eye, EyeOff } from "lucide-react";
|
||||
import RoutePreview from "./route_preview";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import PassThroughSecuritySection from "./common_components/PassThroughSecuritySection";
|
||||
import PassThroughGuardrailsSection from "./common_components/PassThroughGuardrailsSection";
|
||||
|
||||
export interface PassThroughInfoProps {
|
||||
endpointData: PassThroughEndpoint;
|
||||
@@ -37,6 +38,7 @@ interface PassThroughEndpoint {
|
||||
include_subpath?: boolean;
|
||||
cost_per_request?: number;
|
||||
auth?: boolean;
|
||||
guardrails?: Record<string, { request_fields?: string[]; response_fields?: string[] } | null>;
|
||||
}
|
||||
|
||||
// Password field component for headers
|
||||
@@ -68,6 +70,9 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [authEnabled, setAuthEnabled] = useState(initialEndpointData?.auth || false);
|
||||
const [guardrails, setGuardrails] = useState<Record<string, { request_fields?: string[]; response_fields?: string[] } | null>>(
|
||||
initialEndpointData?.guardrails || {}
|
||||
);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleEndpointUpdate = async (values: any) => {
|
||||
@@ -92,6 +97,7 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
||||
include_subpath: values.include_subpath,
|
||||
cost_per_request: values.cost_per_request,
|
||||
auth: premiumUser ? values.auth : undefined,
|
||||
guardrails: guardrails && Object.keys(guardrails).length > 0 ? guardrails : undefined,
|
||||
};
|
||||
|
||||
await updatePassThroughEndpoint(accessToken, endpointData.id, updateData);
|
||||
@@ -214,6 +220,33 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{endpointData.guardrails && Object.keys(endpointData.guardrails).length > 0 && (
|
||||
<Card className="mt-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<Text className="font-medium">Guardrails</Text>
|
||||
<Badge color="purple">{Object.keys(endpointData.guardrails).length} guardrails configured</Badge>
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
{Object.entries(endpointData.guardrails).map(([name, settings]) => (
|
||||
<div key={name} className="p-3 bg-gray-50 rounded">
|
||||
<div className="font-medium text-sm">{name}</div>
|
||||
{settings && (settings.request_fields || settings.response_fields) && (
|
||||
<div className="mt-2 text-xs text-gray-600 space-y-1">
|
||||
{settings.request_fields && (
|
||||
<div>Request fields: {settings.request_fields.join(", ")}</div>
|
||||
)}
|
||||
{settings.response_fields && (
|
||||
<div>Response fields: {settings.response_fields.join(", ")}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!settings && <div className="text-xs text-gray-600 mt-1">Uses entire payload</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
{/* Settings Panel (only for admins) */}
|
||||
@@ -279,6 +312,14 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<PassThroughGuardrailsSection
|
||||
accessToken={accessToken || ""}
|
||||
value={guardrails}
|
||||
onChange={setGuardrails}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-6">
|
||||
<Button onClick={() => setIsEditing(false)}>Cancel</Button>
|
||||
<TremorButton>Save Changes</TremorButton>
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface passThroughItem {
|
||||
include_subpath?: boolean;
|
||||
cost_per_request?: number;
|
||||
auth?: boolean;
|
||||
guardrails?: Record<string, { request_fields?: string[]; response_fields?: string[] } | null>;
|
||||
}
|
||||
|
||||
// Password field component for headers
|
||||
|
||||
Reference in New Issue
Block a user