Permission Management - disable global guardrails by key/team (#16983)

* feat(teams.py): param for disabling guardrails by team

allows use-case where you don't run global guardrails for team - only run team-specific guardrails

* feat(custom_guardrail.py): add support for disabling global guardrails

only run guardrails requested for in the request/key/team

* feat: support adding disable_global_guardrails to metadata if present in key/team metadata

* feat(create_key_button.tsx): new disable global guardrails field

* feat(key_edit_view.tsx): support disabling global guardrails on key edit

* feat(teams.tsx): add disable global guardrails on create team on UI

* feat(team_info.tsx): allow disabling global guardrails on team update
This commit is contained in:
Krish Dholakia
2025-11-22 15:43:50 -08:00
committed by GitHub
parent f7f4320e12
commit e11d34eb69
25 changed files with 274 additions and 28 deletions
+14 -4
View File
@@ -11,9 +11,7 @@ from litellm.types.guardrails import (
Mode,
PiiEntityType,
)
from litellm.types.llms.openai import (
AllMessageValues,
)
from litellm.types.llms.openai import AllMessageValues
from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel
from litellm.types.utils import (
CallTypes,
@@ -136,6 +134,17 @@ class CustomGuardrail(CustomLogger):
f"Event hook {event_hook} is not in the supported event hooks {supported_event_hooks}"
)
def get_disable_global_guardrail(self, data: dict) -> Optional[bool]:
"""
Returns True if the global guardrail should be disabled
"""
if "disable_global_guardrail" in data:
return data["disable_global_guardrail"]
metadata = data.get("litellm_metadata") or data.get("metadata", {})
if "disable_global_guardrail" in metadata:
return metadata["disable_global_guardrail"]
return False
def get_guardrail_from_metadata(
self, data: dict
) -> Union[List[str], List[Dict[str, DynamicGuardrailParams]]]:
@@ -252,6 +261,7 @@ class CustomGuardrail(CustomLogger):
Returns True if the guardrail should be run on the event_type
"""
requested_guardrails = self.get_guardrail_from_metadata(data)
disable_global_guardrail = self.get_disable_global_guardrail(data)
verbose_logger.debug(
"inside should_run_guardrail for guardrail=%s event_type= %s guardrail_supported_event_hooks= %s requested_guardrails= %s self.default_on= %s",
self.guardrail_name,
@@ -260,7 +270,7 @@ class CustomGuardrail(CustomLogger):
requested_guardrails,
self.default_on,
)
if self.default_on is True:
if self.default_on is True and disable_global_guardrail is not True:
if self._event_hook_is_event_type(event_type):
if isinstance(self.event_hook, Mode):
try:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
View File
@@ -812,6 +812,7 @@ class KeyRequestBase(GenerateRequestBase):
key: Optional[str] = None
budget_id: Optional[str] = None
tags: Optional[List[str]] = None
disable_global_guardrails: Optional[bool] = None
enforced_params: Optional[List[str]] = None
allowed_routes: Optional[list] = []
allowed_passthrough_routes: Optional[list] = None
@@ -1357,6 +1358,7 @@ class NewTeamRequest(TeamBase):
prompts: Optional[List[str]] = None
object_permission: Optional[LiteLLM_ObjectPermissionBase] = None
allowed_passthrough_routes: Optional[list] = None
disable_global_guardrails: Optional[bool] = None
model_rpm_limit: Optional[Dict[str, int]] = None
rpm_limit_type: Optional[
Literal["guaranteed_throughput", "best_effort_throughput"]
@@ -1418,6 +1420,7 @@ class UpdateTeamRequest(LiteLLMPydanticObjectBase):
model_aliases: Optional[dict] = None
guardrails: Optional[List[str]] = None
object_permission: Optional[LiteLLM_ObjectPermissionBase] = None
disable_global_guardrails: Optional[bool] = None
team_member_budget: Optional[float] = None
team_member_rpm_limit: Optional[int] = None
team_member_tpm_limit: Optional[int] = None
@@ -3257,6 +3260,7 @@ LiteLLM_ManagementEndpoint_MetadataFields = [
]
LiteLLM_ManagementEndpoint_MetadataFields_Premium = [
"disable_global_guardrails",
"guardrails",
"tags",
"team_member_key_duration",
+39 -13
View File
@@ -585,7 +585,6 @@ class LiteLLMProxyRequestSetup:
if user_api_key_dict.budget_reset_at
else None
),
user_api_key_auth_metadata=user_api_key_dict.metadata,
)
return user_api_key_logged_metadata
@@ -668,6 +667,12 @@ class LiteLLMProxyRequestSetup:
tags_to_add=key_metadata["tags"],
)
)
if "disable_global_guardrails" in key_metadata and isinstance(
key_metadata["disable_global_guardrails"], bool
):
data[_metadata_variable_name]["disable_global_guardrails"] = key_metadata[
"disable_global_guardrails"
]
if "spend_logs_metadata" in key_metadata and isinstance(
key_metadata["spend_logs_metadata"], dict
):
@@ -936,6 +941,12 @@ async def add_litellm_data_to_request( # noqa: PLR0915
request_tags=data[_metadata_variable_name].get("tags"),
tags_to_add=team_metadata["tags"],
)
if "disable_global_guardrails" in team_metadata and isinstance(
team_metadata["disable_global_guardrails"], bool
):
data[_metadata_variable_name]["disable_global_guardrails"] = team_metadata[
"disable_global_guardrails"
]
if "spend_logs_metadata" in team_metadata and isinstance(
team_metadata["spend_logs_metadata"], dict
):
@@ -1240,7 +1251,7 @@ def _add_guardrails_from_key_or_team_metadata(
) -> None:
"""
Helper add guardrails from key or team metadata to request data
Key guardrails are set first, then team guardrails are appended (without duplicates).
Args:
@@ -1254,19 +1265,25 @@ def _add_guardrails_from_key_or_team_metadata(
# Initialize guardrails set (avoiding duplicates)
combined_guardrails = set()
# Add key-level guardrails first
if key_metadata and "guardrails" in key_metadata:
if isinstance(key_metadata["guardrails"], list) and len(key_metadata["guardrails"]) > 0:
if (
isinstance(key_metadata["guardrails"], list)
and len(key_metadata["guardrails"]) > 0
):
_premium_user_check()
combined_guardrails.update(key_metadata["guardrails"])
# Add team-level guardrails (set automatically handles duplicates)
if team_metadata and "guardrails" in team_metadata:
if isinstance(team_metadata["guardrails"], list) and len(team_metadata["guardrails"]) > 0:
if (
isinstance(team_metadata["guardrails"], list)
and len(team_metadata["guardrails"]) > 0
):
_premium_user_check()
combined_guardrails.update(team_metadata["guardrails"])
# Set combined guardrails in metadata as list
if combined_guardrails:
data[metadata_variable_name]["guardrails"] = list(combined_guardrails)
@@ -1292,23 +1309,32 @@ def move_guardrails_to_metadata(
)
#########################################################################################
# User's might send "guardrails" in the request body, we need to add them to the request metadata.
# User's might send "guardrails" in the request body, we need to add them to the request metadata.
# Since downstream logic requires "guardrails" to be in the request metadata
#########################################################################################
if "guardrails" in data:
request_body_guardrails = data.pop("guardrails")
if "guardrails" in data[_metadata_variable_name] and isinstance(data[_metadata_variable_name]["guardrails"], list):
if "guardrails" in data[_metadata_variable_name] and isinstance(
data[_metadata_variable_name]["guardrails"], list
):
data[_metadata_variable_name]["guardrails"].extend(request_body_guardrails)
else:
data[_metadata_variable_name]["guardrails"] = request_body_guardrails
#########################################################################################
if "guardrail_config" in data:
request_body_guardrail_config = data.pop("guardrail_config")
if "guardrail_config" in data[_metadata_variable_name] and isinstance(data[_metadata_variable_name]["guardrail_config"], dict):
data[_metadata_variable_name]["guardrail_config"].update(request_body_guardrail_config)
if "guardrail_config" in data[_metadata_variable_name] and isinstance(
data[_metadata_variable_name]["guardrail_config"], dict
):
data[_metadata_variable_name]["guardrail_config"].update(
request_body_guardrail_config
)
else:
data[_metadata_variable_name]["guardrail_config"] = request_body_guardrail_config
data[_metadata_variable_name][
"guardrail_config"
] = request_body_guardrail_config
def add_provider_specific_headers_to_request(
data: dict,
@@ -688,8 +688,7 @@ async def new_team( # noqa: PLR0915
},
)
if (data.max_budget is not None and user_api_key_dict.user_id is not None):
if data.max_budget is not None and user_api_key_dict.user_id is not None:
# Fetch user object to get max_budget
user_obj = await get_user_object(
user_id=user_api_key_dict.user_id,
@@ -699,7 +698,7 @@ async def new_team( # noqa: PLR0915
)
if (
user_obj is not None
user_obj is not None
and user_obj.max_budget is not None
and data.max_budget > user_obj.max_budget
):
@@ -171,6 +171,79 @@ class TestCustomGuardrailShouldRunGuardrail:
assert result is False
def test_should_run_guardrail_with_disable_global_guardrail(self):
"""Test that disable_global_guardrail disables a global guardrail when set to True"""
from litellm.types.guardrails import GuardrailEventHooks
# Create a guardrail with default_on=True (global guardrail)
custom_guardrail = CustomGuardrail(
guardrail_name="global_guardrail",
default_on=True,
event_hook=GuardrailEventHooks.pre_call,
)
# Test 1: Global guardrail runs by default when default_on=True
data = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
}
result = custom_guardrail.should_run_guardrail(
data=data, event_type=GuardrailEventHooks.pre_call
)
assert result is True, "Global guardrail should run when default_on=True"
# Test 2: Global guardrail is disabled when disable_global_guardrail=True at root level
data_with_disable_root = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"disable_global_guardrail": True,
}
result = custom_guardrail.should_run_guardrail(
data=data_with_disable_root, event_type=GuardrailEventHooks.pre_call
)
assert (
result is False
), "Global guardrail should be disabled when disable_global_guardrail=True"
# Test 3: Global guardrail is disabled when disable_global_guardrail=True in litellm_metadata
data_with_disable_litellm = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"litellm_metadata": {"disable_global_guardrail": True},
}
result = custom_guardrail.should_run_guardrail(
data=data_with_disable_litellm, event_type=GuardrailEventHooks.pre_call
)
assert (
result is False
), "Global guardrail should be disabled when disable_global_guardrail=True in litellm_metadata"
# Test 4: Global guardrail is disabled when disable_global_guardrail=True in metadata
data_with_disable_metadata = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"metadata": {"disable_global_guardrail": True},
}
result = custom_guardrail.should_run_guardrail(
data=data_with_disable_metadata, event_type=GuardrailEventHooks.pre_call
)
assert (
result is False
), "Global guardrail should be disabled when disable_global_guardrail=True in metadata"
# Test 5: Global guardrail runs when disable_global_guardrail=False
data_with_disable_false = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"disable_global_guardrail": False,
}
result = custom_guardrail.should_run_guardrail(
data=data_with_disable_false, event_type=GuardrailEventHooks.pre_call
)
assert (
result is True
), "Global guardrail should still run when disable_global_guardrail=False"
class TestApplyGuardrailCheck:
def test_apply_guardrail_check_only_on_direct_implementation(self):
@@ -304,7 +377,9 @@ class TestGuardrailLoggingAggregation:
self._invoke_add_log(request_data)
info = request_data["litellm_metadata"]["standard_logging_guardrail_information"]
info = request_data["litellm_metadata"][
"standard_logging_guardrail_information"
]
assert isinstance(info, list)
assert len(info) == 2
assert info[1]["guardrail_name"] == "test_guardrail"
@@ -1,4 +1,4 @@
import { Button as Button2, Form, Input, Modal, Select as Select2, Tooltip } from "antd";
import { Button as Button2, Form, Input, Modal, Select as Select2, Switch, Tooltip } from "antd";
import { Accordion, AccordionBody, AccordionHeader, Text, TextInput } from "@tremor/react";
import { InfoCircleOutlined } from "@ant-design/icons";
import {
@@ -452,6 +452,25 @@ const CreateTeamModal = ({
}))}
/>
</Form.Item>
<Form.Item
label={
<span>
Disable Global Guardrails{" "}
<Tooltip title="When enabled, this team will bypass any guardrails configured to run on every request (global guardrails)">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="disable_global_guardrails"
className="mt-4"
valuePropName="checked"
help="Bypass global guardrails for this team"
>
<Switch
checkedChildren="Yes"
unCheckedChildren="No"
/>
</Form.Item>
<Form.Item
label={
<span>
@@ -30,7 +30,7 @@ import {
Text,
TextInput,
} from "@tremor/react";
import { Button as Button2, Form, Input, Modal, Select as Select2, Tooltip, Typography } from "antd";
import { Button as Button2, Form, Input, Modal, Select as Select2, Switch, Tooltip, Typography } from "antd";
import React, { useEffect, useState } from "react";
import { formatNumberWithCommas } from "../utils/dataUtils";
import { fetchTeams } from "./common_components/fetch_teams";
@@ -1262,6 +1262,26 @@ const Teams: React.FC<TeamProps> = ({
}))}
/>
</Form.Item>
<Form.Item
label={
<span>
Disable Global Guardrails{" "}
<Tooltip title="When enabled, this team will bypass any guardrails configured to run on every request (global guardrails)">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="disable_global_guardrails"
className="mt-4"
valuePropName="checked"
help="Bypass global guardrails for this team"
>
<Switch
disabled={!premiumUser}
checkedChildren={premiumUser ? "Yes" : "Premium feature - Upgrade to disable global guardrails by team"}
unCheckedChildren={premiumUser ? "No" : "Premium feature - Upgrade to disable global guardrails by team"}
/>
</Form.Item>
<Form.Item
label={
<span>
@@ -3,7 +3,7 @@ import React, { useState, useEffect, useCallback } from "react";
import { Button, TextInput, Grid, Col } from "@tremor/react";
import { Text, Title, Accordion, AccordionHeader, AccordionBody } from "@tremor/react";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { Button as Button2, Modal, Form, Input, Select, Radio } from "antd";
import { Button as Button2, Modal, Form, Input, Select, Radio, Switch } from "antd";
import NumericalInput from "../shared/numerical_input";
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
import SchemaFormFields from "../common_components/check_openapi_schema";
@@ -879,6 +879,37 @@ const CreateKey: React.FC<CreateKeyProps> = ({
options={guardrailsList.map((name) => ({ value: name, label: name }))}
/>
</Form.Item>
<Form.Item
label={
<span>
Disable Global Guardrails{" "}
<Tooltip title="When enabled, this key will bypass any guardrails configured to run on every request (global guardrails)">
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</span>
}
name="disable_global_guardrails"
className="mt-4"
valuePropName="checked"
help={
premiumUser
? "Bypass global guardrails for this key"
: "Premium feature - Upgrade to disable global guardrails by key"
}
>
<Switch
disabled={!premiumUser}
checkedChildren="Yes"
unCheckedChildren="No"
/>
</Form.Item>
<Form.Item
label={
<span>
@@ -25,7 +25,7 @@ import {
teamUpdateCall,
getGuardrailsList,
} from "@/components/networking";
import { Button, Form, Input, Select, message, Modal, Tooltip } from "antd";
import { Button, Form, Input, Select, Switch, message, Modal, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import MemberModal from "./edit_membership";
@@ -558,6 +558,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
team_member_tpm_limit: info.team_member_budget_table?.tpm_limit,
team_member_rpm_limit: info.team_member_budget_table?.rpm_limit,
guardrails: info.metadata?.guardrails || [],
disable_global_guardrails: info.metadata?.disable_global_guardrails || false,
metadata: info.metadata
? JSON.stringify((({ logging, ...rest }) => rest)(info.metadata), null, 2)
: "",
@@ -673,6 +674,25 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
/>
</Form.Item>
<Form.Item
label={
<span>
Disable Global Guardrails{" "}
<Tooltip title="When enabled, this team will bypass any guardrails configured to run on every request (global guardrails)">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="disable_global_guardrails"
valuePropName="checked"
help="Bypass global guardrails for this team"
>
<Switch
checkedChildren="Yes"
unCheckedChildren="No"
/>
</Form.Item>
<Form.Item label="Vector Stores" name="vector_stores">
<VectorStoreSelector
onChange={(values: string[]) => form.setFieldValue("vector_stores", values)}
@@ -806,6 +826,17 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<Badge color={info.blocked ? "red" : "green"}>{info.blocked ? "Blocked" : "Active"}</Badge>
</div>
<div>
<Text className="font-medium">Disable Global Guardrails</Text>
<div>
{info.metadata?.disable_global_guardrails === true ? (
<Badge color="yellow">Enabled - Global guardrails bypassed</Badge>
) : (
<Badge color="green">Disabled - Global guardrails active</Badge>
)}
</div>
</div>
<ObjectPermissionsView
objectPermission={info.object_permission}
variant="inline"
@@ -1,6 +1,6 @@
import GuardrailSelector from "@/components/guardrails/GuardrailSelector";
import { TextInput, Button as TremorButton } from "@tremor/react";
import { Form, Input, Select, Tooltip } from "antd";
import { Form, Input, Select, Switch, Tooltip } from "antd";
import { useEffect, useState } from "react";
import { mapInternalToDisplayNames } from "../callback_info_helpers";
import KeyLifecycleSettings from "../common_components/KeyLifecycleSettings";
@@ -17,6 +17,7 @@ import NumericalInput from "../shared/numerical_input";
import { Tag } from "../tag_management/types";
import EditLoggingSettings from "../team/EditLoggingSettings";
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
import { InfoCircleOutlined } from "@ant-design/icons";
interface KeyEditViewProps {
keyData: KeyResponse;
@@ -164,6 +165,7 @@ export function KeyEditView({
budget_duration: getBudgetDuration(keyData.budget_duration),
metadata: formatMetadataForDisplay(stripTagsFromMetadata(keyData.metadata)),
guardrails: keyData.metadata?.guardrails,
disable_global_guardrails: keyData.metadata?.disable_global_guardrails || false,
prompts: keyData.metadata?.prompts,
tags: keyData.metadata?.tags,
vector_stores: keyData.object_permission?.vector_stores || [],
@@ -188,6 +190,7 @@ export function KeyEditView({
budget_duration: getBudgetDuration(keyData.budget_duration),
metadata: formatMetadataForDisplay(stripTagsFromMetadata(keyData.metadata)),
guardrails: keyData.metadata?.guardrails,
disable_global_guardrails: keyData.metadata?.disable_global_guardrails || false,
prompts: keyData.metadata?.prompts,
tags: keyData.metadata?.tags,
vector_stores: keyData.object_permission?.vector_stores || [],
@@ -388,6 +391,25 @@ export function KeyEditView({
)}
</Form.Item>
<Form.Item
label={
<span>
Disable Global Guardrails{" "}
<Tooltip title="When enabled, this key will bypass any guardrails configured to run on every request (global guardrails)">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="disable_global_guardrails"
valuePropName="checked"
>
<Switch
disabled={!premiumUser}
checkedChildren="Yes"
unCheckedChildren="No"
/>
</Form.Item>
<Form.Item label="Tags" name="tags">
<Select
mode="tags"
@@ -673,6 +673,17 @@ export default function KeyInfoView({
</Text>
</div>
<div>
<Text className="font-medium">Disable Global Guardrails</Text>
<Text>
{currentKeyData.metadata?.disable_global_guardrails === true ? (
<Badge color="yellow">Enabled - Global guardrails bypassed</Badge>
) : (
<Badge color="green">Disabled - Global guardrails active</Badge>
)}
</Text>
</div>
<div>
<Text className="font-medium">Models</Text>
<div className="flex flex-wrap gap-2 mt-1">