Merge pull request #25575 from BerriAI/litellm_feat-per-guardrail-opt-out-for-global-guardrails

feat(guardrails): per-team opt-out for specific global guardrails
This commit is contained in:
ryan-crabbe-berri
2026-04-13 13:31:23 -07:00
committed by GitHub
13 changed files with 457 additions and 84 deletions
+3 -1
View File
@@ -51,7 +51,9 @@ LiteLLM is a unified interface for 100+ LLMs that:
### MAKING CODE CHANGES FOR THE UI (IGNORE FOR BACKEND)
1. **Tremor is DEPRECATED, do not use Tremor components in new features/changes**
1. **Always use `antd` for new UI components — Tremor is DEPRECATED**
- We are migrating off of `@tremor/react`. Do not introduce new `Badge`, `Text`, `Card`, `Grid`, `Title`, or other imports from `@tremor/react` in any new or modified file.
- Use `antd` equivalents: `Tag` for labels, plain `<span>`/`<div>` with Tailwind classes (or `Typography.Text`) for text, `Card` from `antd`, etc. Note that `antd` has no `"yellow"` Tag color — use `"gold"` for amber/yellow.
- The only exception is the Tremor Table component and its required Tremor Table sub components.
2. **Use Common Components as much as possible**:
+3
View File
@@ -109,6 +109,9 @@ LiteLLM is a unified interface for 100+ LLM providers with two main components:
### UI / Backend Consistency
- When wiring a new UI entity type to an existing backend endpoint, verify the backend API contract (single value vs. array, required vs. optional params) and ensure the UI controls match — e.g., use a single-select dropdown when the backend accepts a single value, not a multi-select
### UI Component Library
- **Always use `antd` for new UI components** — we are migrating off of `@tremor/react`. Do not introduce new `Badge`, `Text`, `Card`, `Grid`, `Title`, or other imports from `@tremor/react` in any new or modified file. Use `antd` equivalents: `Tag` for labels, plain `<span>`/`<div>` with Tailwind classes (or `Typography.Text`) for text, `Card` from `antd`, etc. Note that `antd` has no `"yellow"` Tag color — use `"gold"` for amber/yellow.
### MCP OAuth / OpenAPI Transport Mapping
- `TRANSPORT.OPENAPI` is a UI-only concept. The backend only accepts `"http"`, `"sse"`, or `"stdio"`. Always map it to `"http"` before any API call (including pre-OAuth temp-session calls).
- FastAPI validation errors return `detail` as an array of `{loc, msg, type}` objects. Error extractors must handle: array (map `.msg`), string, nested `{error: string}`, and fallback.
+19 -4
View File
@@ -259,13 +259,24 @@ class CustomGuardrail(CustomLogger):
"""
Returns True if the global guardrail should be disabled
"""
if "disable_global_guardrail" in data:
return data["disable_global_guardrail"]
if "disable_global_guardrails" in data:
return data["disable_global_guardrails"]
metadata = data.get("litellm_metadata") or data.get("metadata", {})
if "disable_global_guardrail" in metadata:
return metadata["disable_global_guardrail"]
if "disable_global_guardrails" in metadata:
return metadata["disable_global_guardrails"]
return False
def get_opted_out_global_guardrails_from_metadata(self, data: dict) -> List[str]:
"""
Returns the list of global guardrail names the team/key has opted out of.
"""
if "opted_out_global_guardrails" in data:
value = data["opted_out_global_guardrails"]
return value if isinstance(value, list) else []
metadata = data.get("litellm_metadata") or data.get("metadata", {})
value = metadata.get("opted_out_global_guardrails")
return value if isinstance(value, list) else []
def _is_valid_response_type(self, result: Any) -> bool:
"""
Check if result is a valid LLMResponseTypes instance.
@@ -406,6 +417,7 @@ class CustomGuardrail(CustomLogger):
"""
requested_guardrails = self.get_guardrail_from_metadata(data)
disable_global_guardrail = self.get_disable_global_guardrail(data)
opted_out_global_guardrails = self.get_opted_out_global_guardrails_from_metadata(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,
@@ -414,6 +426,9 @@ class CustomGuardrail(CustomLogger):
requested_guardrails,
self.default_on,
)
if self.default_on is True and self.guardrail_name in opted_out_global_guardrails:
return False
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):
+6
View File
@@ -1103,6 +1103,12 @@ async def add_litellm_data_to_request( # noqa: PLR0915
data[_metadata_variable_name]["disable_global_guardrails"] = team_metadata[
"disable_global_guardrails"
]
if "opted_out_global_guardrails" in team_metadata and isinstance(
team_metadata["opted_out_global_guardrails"], list
):
data[_metadata_variable_name]["opted_out_global_guardrails"] = team_metadata[
"opted_out_global_guardrails"
]
if "spend_logs_metadata" in team_metadata and isinstance(
team_metadata["spend_logs_metadata"], dict
):
@@ -197,7 +197,7 @@ class TestCustomGuardrailShouldRunGuardrail:
data_with_disable_root = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"disable_global_guardrail": True,
"disable_global_guardrails": True,
}
result = custom_guardrail.should_run_guardrail(
data=data_with_disable_root, event_type=GuardrailEventHooks.pre_call
@@ -210,7 +210,7 @@ class TestCustomGuardrailShouldRunGuardrail:
data_with_disable_litellm = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"litellm_metadata": {"disable_global_guardrail": True},
"litellm_metadata": {"disable_global_guardrails": True},
}
result = custom_guardrail.should_run_guardrail(
data=data_with_disable_litellm, event_type=GuardrailEventHooks.pre_call
@@ -223,7 +223,7 @@ class TestCustomGuardrailShouldRunGuardrail:
data_with_disable_metadata = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"metadata": {"disable_global_guardrail": True},
"metadata": {"disable_global_guardrails": True},
}
result = custom_guardrail.should_run_guardrail(
data=data_with_disable_metadata, event_type=GuardrailEventHooks.pre_call
@@ -236,7 +236,7 @@ class TestCustomGuardrailShouldRunGuardrail:
data_with_disable_false = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"disable_global_guardrail": False,
"disable_global_guardrails": False,
}
result = custom_guardrail.should_run_guardrail(
data=data_with_disable_false, event_type=GuardrailEventHooks.pre_call
@@ -245,6 +245,121 @@ class TestCustomGuardrailShouldRunGuardrail:
result is True
), "Global guardrail should still run when disable_global_guardrail=False"
def test_should_run_guardrail_with_opted_out_global_guardrails(self):
"""Test the per-guardrail opt-out list for global (default_on=True) guardrails"""
from litellm.types.guardrails import GuardrailEventHooks
custom_guardrail = CustomGuardrail(
guardrail_name="global_guardrail",
default_on=True,
event_hook=GuardrailEventHooks.pre_call,
)
# Test 1: guardrail in the opt-out list at root level → skipped
data_root = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"opted_out_global_guardrails": ["global_guardrail"],
}
assert (
custom_guardrail.should_run_guardrail(
data=data_root, event_type=GuardrailEventHooks.pre_call
)
is False
)
# Test 2: guardrail in the opt-out list inside litellm_metadata → skipped
data_litellm = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"litellm_metadata": {"opted_out_global_guardrails": ["global_guardrail"]},
}
assert (
custom_guardrail.should_run_guardrail(
data=data_litellm, event_type=GuardrailEventHooks.pre_call
)
is False
)
# Test 3: guardrail in the opt-out list inside metadata → skipped
data_metadata = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"metadata": {"opted_out_global_guardrails": ["global_guardrail"]},
}
assert (
custom_guardrail.should_run_guardrail(
data=data_metadata, event_type=GuardrailEventHooks.pre_call
)
is False
)
# Test 4: a different guardrail in the opt-out list → still runs
data_other = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"metadata": {"opted_out_global_guardrails": ["some_other_guardrail"]},
}
assert (
custom_guardrail.should_run_guardrail(
data=data_other, event_type=GuardrailEventHooks.pre_call
)
is True
)
# Test 5: empty opt-out list → still runs
data_empty = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"metadata": {"opted_out_global_guardrails": []},
}
assert (
custom_guardrail.should_run_guardrail(
data=data_empty, event_type=GuardrailEventHooks.pre_call
)
is True
)
# Test 6: malformed value (bool instead of list) → safely ignored, guardrail runs
data_malformed = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"metadata": {"opted_out_global_guardrails": True},
}
assert (
custom_guardrail.should_run_guardrail(
data=data_malformed, event_type=GuardrailEventHooks.pre_call
)
is True
)
def test_should_run_guardrail_opt_out_does_not_affect_non_global(self):
"""Opt-out list only matters for default_on=True guardrails"""
from litellm.types.guardrails import GuardrailEventHooks
non_global = CustomGuardrail(
guardrail_name="opt_in_guardrail",
default_on=False,
event_hook=GuardrailEventHooks.pre_call,
)
# An opt-in guardrail named in opted_out_global_guardrails is still controlled
# by the explicit `guardrails` request list, not by the global opt-out list.
data = {
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "test"}],
"metadata": {
"opted_out_global_guardrails": ["opt_in_guardrail"],
"guardrails": ["opt_in_guardrail"],
},
}
assert (
non_global.should_run_guardrail(
data=data, event_type=GuardrailEventHooks.pre_call
)
is True
)
class TestApplyGuardrailCheck:
def test_apply_guardrail_check_only_on_direct_implementation(self):
@@ -2263,7 +2263,7 @@ class TestPanwAirsMcpForceRun:
"test_panw_airs",
False,
"pre_call",
_simple_data(disable_global_guardrail=True),
_simple_data(disable_global_guardrails=True),
GuardrailEventHooks.pre_mcp_call,
False,
id="honors_disable_global_on_mcp_hooks",
@@ -74,7 +74,7 @@ describe("useGuardrails", () => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(expectedGuardrailNames);
expect(result.current.data?.guardrails.map((g) => g.guardrail_name)).toEqual(expectedGuardrailNames);
expect(result.current.error).toBeNull();
expect(getGuardrailsList).toHaveBeenCalledWith("test-access-token");
expect(getGuardrailsList).toHaveBeenCalledTimes(1);
@@ -228,7 +228,7 @@ describe("useGuardrails", () => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual([]);
expect(result.current.data?.guardrails).toEqual([]);
expect(getGuardrailsList).toHaveBeenCalledWith("test-access-token");
});
@@ -265,9 +265,35 @@ describe("useGuardrails", () => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(expectedNames);
expect(result.current.data).toHaveLength(2);
expect(result.current.data).toContain("custom-guardrail-1");
expect(result.current.data).toContain("custom-guardrail-2");
const names = result.current.data?.guardrails.map((g) => g.guardrail_name);
expect(names).toEqual(expectedNames);
expect(names).toHaveLength(2);
expect(names).toContain("custom-guardrail-1");
expect(names).toContain("custom-guardrail-2");
});
it("should partition guardrails into global and optional sets based on default_on", async () => {
const mockMixedResponse = {
guardrails: [
{ guardrail_name: "global-guard-a", litellm_params: { default_on: true } },
{ guardrail_name: "global-guard-b", litellm_params: { default_on: true } },
{ guardrail_name: "optional-guard-a", litellm_params: { default_on: false } },
{ guardrail_name: "optional-guard-b" },
],
};
(getGuardrailsList as any).mockResolvedValue(mockMixedResponse);
const { result } = renderHook(() => useGuardrails(), { wrapper });
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.globalGuardrailNames).toEqual(
new Set(["global-guard-a", "global-guard-b"]),
);
expect(result.current.data?.optionalGuardrailNames).toEqual(
new Set(["optional-guard-a", "optional-guard-b"]),
);
});
});
@@ -3,16 +3,52 @@ import { createQueryKeys } from "../common/queryKeysFactory";
import { getGuardrailsList } from "@/components/networking";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
// ── Types ────────────────────────────────────────────────────────────────────
export interface GuardrailListItem {
guardrail_name: string;
litellm_params?: {
default_on?: boolean;
mode?: string | string[];
[key: string]: unknown;
};
guardrail_info?: Record<string, unknown> | null;
guardrail_id?: string | null;
[key: string]: unknown;
}
interface GuardrailsListResponse {
guardrails: GuardrailListItem[];
}
export interface GuardrailsListData {
guardrails: GuardrailListItem[];
globalGuardrailNames: Set<string>;
optionalGuardrailNames: Set<string>;
}
// ── Hook ─────────────────────────────────────────────────────────────────────
const guardrailKeys = createQueryKeys("guardrails");
export const useGuardrails = (): UseQueryResult<string[]> => {
export const useGuardrails = (): UseQueryResult<GuardrailsListData> => {
const { accessToken, userId, userRole } = useAuthorized();
return useQuery<string[]>({
return useQuery<GuardrailsListResponse, Error, GuardrailsListData>({
queryKey: guardrailKeys.list({}),
queryFn: async () => {
const response = await getGuardrailsList(accessToken!);
return response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name);
},
queryFn: async () => getGuardrailsList(accessToken!),
enabled: Boolean(accessToken && userId && userRole),
select: (data) => {
const guardrails: GuardrailListItem[] = data?.guardrails ?? [];
const globalGuardrailNames = new Set<string>();
const optionalGuardrailNames = new Set<string>();
for (const g of guardrails) {
if (g.litellm_params?.default_on) {
globalGuardrailNames.add(g.guardrail_name);
} else {
optionalGuardrailNames.add(g.guardrail_name);
}
}
return { guardrails, globalGuardrailNames, optionalGuardrailNames };
},
});
};
@@ -0,0 +1,97 @@
import React from "react";
import { Tag } from "antd";
import { GlobalOutlined } from "@ant-design/icons";
interface GuardrailSettingsViewProps {
globalGuardrailNames: Set<string>;
teamGuardrails?: string[];
optedOutGlobalGuardrails?: string[];
killSwitchOn?: boolean;
variant?: "card" | "inline";
className?: string;
}
export function GuardrailSettingsView({
globalGuardrailNames,
teamGuardrails = [],
optedOutGlobalGuardrails = [],
killSwitchOn = false,
variant = "card",
className = "",
}: GuardrailSettingsViewProps) {
const optedOutSet = new Set(optedOutGlobalGuardrails);
const globalsRunning = Array.from(globalGuardrailNames).filter(
(n) => !optedOutSet.has(n),
);
const nonGlobalOptIns = teamGuardrails.filter(
(n) => !globalGuardrailNames.has(n),
);
const isEmpty =
!killSwitchOn && globalsRunning.length === 0 && nonGlobalOptIns.length === 0;
const content = isEmpty ? (
<span className="block text-gray-500">No guardrails configured</span>
) : (
<div className="flex flex-col gap-4">
<div>
<span className="block text-sm font-medium text-gray-700 mb-2">
<GlobalOutlined style={{ marginInlineEnd: 4 }} aria-label="Global guardrail" />
Global
</span>
{killSwitchOn ? (
<Tag color="gold">Bypassed for this team</Tag>
) : globalsRunning.length > 0 ? (
<div className="flex flex-wrap gap-2">
{globalsRunning.map((name) => (
<Tag key={name} color="blue">
{name}
</Tag>
))}
</div>
) : (
<span className="block text-sm text-gray-500">None configured</span>
)}
</div>
<div>
<span className="block text-sm font-medium text-gray-700 mb-2">Team-specific</span>
{nonGlobalOptIns.length > 0 ? (
<div className="flex flex-wrap gap-2">
{nonGlobalOptIns.map((name) => (
<Tag key={name} color="blue">
{name}
</Tag>
))}
</div>
) : (
<span className="block text-sm text-gray-500">None configured</span>
)}
</div>
</div>
);
if (variant === "card") {
return (
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}>
<div className="flex items-center gap-2 mb-6">
<div>
<span className="block font-semibold text-gray-900">Guardrails Settings</span>
<span className="block text-xs text-gray-500">
Global and team-specific guardrails applied to this team
</span>
</div>
</div>
{content}
</div>
);
}
return (
<div className={`${className}`}>
<span className="block font-medium text-gray-900 mb-3">Guardrails Settings</span>
{content}
</div>
);
}
export default GuardrailSettingsView;
@@ -82,7 +82,11 @@ vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({
vi.mock("@/app/(dashboard)/hooks/guardrails/useGuardrails", () => ({
useGuardrails: vi.fn().mockReturnValue({
data: [{ guardrail_name: "test-guardrail" }],
data: {
guardrails: [{ guardrail_name: "test-guardrail" }],
globalGuardrailNames: new Set<string>(),
optionalGuardrailNames: new Set<string>(["test-guardrail"]),
},
isLoading: false,
error: null,
}),
@@ -63,7 +63,8 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
isLoading: isProviderMetadataLoading,
error: providerMetadataError,
} = useProviderFields();
const { data: guardrailsList, isLoading: isGuardrailsLoading, error: guardrailsError } = useGuardrails();
const { data: guardrailsData } = useGuardrails();
const guardrailsList = guardrailsData?.guardrails.map((g) => g.guardrail_name);
const { data: tagsList, isLoading: isTagsLoading, error: tagsError } = useTags();
const handleTestConnection = async () => {
@@ -276,15 +276,17 @@ describe("TeamInfoView", () => {
it("should display guardrails in overview when present", async () => {
vi.mocked(networking.teamInfoCall).mockResolvedValue(
createMockTeamData({
guardrails: ["guardrail1", "guardrail2"],
metadata: { guardrails: ["guardrail1", "guardrail2"] },
})
);
renderWithProviders(<TeamInfoView {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("Guardrails")).toBeInTheDocument();
expect(screen.getByText("Guardrails Settings")).toBeInTheDocument();
});
expect(screen.getByText("guardrail1")).toBeInTheDocument();
expect(screen.getByText("guardrail2")).toBeInTheDocument();
});
it("should display policies in overview when present", async () => {
@@ -2,7 +2,6 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import UserSearchModal from "@/components/common_components/user_search_modal";
import {
getGuardrailsList,
getPoliciesList,
getPolicyInfoWithGuardrails,
Member,
@@ -14,13 +13,14 @@ import {
teamMemberUpdateCall,
teamUpdateCall,
} from "@/components/networking";
import { useGuardrails } from "@/app/(dashboard)/hooks/guardrails/useGuardrails";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
import { isProxyAdminRole } from "@/utils/roles";
import { EditOutlined, InfoCircleOutlined, MinusCircleOutlined, PlusOutlined, SaveOutlined } from "@ant-design/icons";
import { EditOutlined, GlobalOutlined, InfoCircleOutlined, MinusCircleOutlined, PlusOutlined, SaveOutlined } from "@ant-design/icons";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import { Badge, Card, Grid, Text, TextInput, Title } from "@tremor/react";
import { Button, Form, Input, InputNumber, Select, Space, Switch, Tabs, Tooltip } from "antd";
import { Button, Form, Input, InputNumber, Select, Space, Switch, Tabs, Tag, Tooltip } from "antd";
import MessageManager from "@/components/molecules/message_manager";
import { CheckIcon, CopyIcon } from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
@@ -31,6 +31,7 @@ import DeleteResourceModal from "../common_components/DeleteResourceModal";
import DurationSelect from "../common_components/DurationSelect";
import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector";
import { unfurlWildcardModelsInList } from "../key_team_helpers/fetch_available_models_team_key";
import GuardrailSettingsView from "../GuardrailSettingsView";
import LoggingSettingsView from "../logging_settings_view";
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
@@ -179,7 +180,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
const { data: guardrailsData, isLoading: isGuardrailsLoading } = useGuardrails();
const globalGuardrailNames = guardrailsData?.globalGuardrailNames ?? new Set<string>();
const [policiesList, setPoliciesList] = useState<string[]>([]);
const [policyGuardrails, setPolicyGuardrails] = useState<Record<string, string[]>>({});
const [loadingPolicies, setLoadingPolicies] = useState(false);
@@ -202,6 +204,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
// Models currently selected in the team edit form, used to scope the per-model
// rate limit dropdown to models this team actually has access to.
const selectedModelsInForm = Form.useWatch("models", form) as string[] | undefined;
const killSwitchOn = Form.useWatch("disable_global_guardrails", form) as boolean | undefined;
const availableRateLimitModels = useMemo(() => {
const selected = selectedModelsInForm ?? teamData?.team_info?.models ?? [];
if (selected.includes("all-proxy-models") || selected.includes("all-team-models")) {
@@ -273,17 +276,6 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
};
useEffect(() => {
const fetchGuardrails = async () => {
try {
if (!accessToken) return;
const response = await getGuardrailsList(accessToken);
const guardrailNames = response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name);
setGuardrailsList(guardrailNames);
} catch (error) {
console.error("Failed to fetch guardrails:", error);
}
};
const fetchPolicies = async () => {
try {
if (!accessToken) return;
@@ -295,7 +287,6 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
}
};
fetchGuardrails();
fetchPolicies();
}, [accessToken]);
@@ -491,6 +482,13 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
}
}
const killSwitchOnAtSave = values.disable_global_guardrails === true;
const optedOutGlobalGuardrails = killSwitchOnAtSave
? Array.from(globalGuardrailNames)
: Array.from(globalGuardrailNames).filter(
(n) => !(values.guardrails || []).includes(n),
);
const updateData: any = {
team_id: teamId,
team_alias: values.team_alias,
@@ -504,9 +502,10 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
budget_duration: values.budget_duration,
metadata: {
...parsedMetadata,
...(values.guardrails?.length > 0 ? { guardrails: values.guardrails } : {}),
guardrails: (values.guardrails || []).filter((n: string) => !globalGuardrailNames.has(n)),
opted_out_global_guardrails: optedOutGlobalGuardrails,
...(values.logging_settings?.length > 0 ? { logging: values.logging_settings } : {}),
disable_global_guardrails: values.disable_global_guardrails || false,
disable_global_guardrails: killSwitchOnAtSave,
soft_budget_alerting_emails:
typeof values.soft_budget_alerting_emails === "string"
? values.soft_budget_alerting_emails
@@ -610,6 +609,39 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
const { team_info: info } = teamData;
const initialKillSwitchOn = info.metadata?.disable_global_guardrails === true;
const optedOutGlobals = new Set<string>(info.metadata?.opted_out_global_guardrails || []);
const nonGlobalOptIns: string[] = (info.metadata?.guardrails || []).filter(
(n: string) => !globalGuardrailNames.has(n),
);
const effectiveGuardrails: string[] = initialKillSwitchOn
? nonGlobalOptIns
: [
...Array.from(globalGuardrailNames).filter((n) => !optedOutGlobals.has(n)),
...nonGlobalOptIns,
];
const preventTagMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
};
const renderGuardrailTag = ({ label, value, closable, onClose }: any) => {
const isGlobal = globalGuardrailNames.has(value);
return (
<Tag
color="blue"
closable={closable}
onClose={onClose}
onMouseDown={preventTagMouseDown}
style={{ marginInlineEnd: 4 }}
>
{isGlobal && <GlobalOutlined style={{ marginInlineEnd: 4 }} aria-label="Global guardrail" />}
{label}
</Tag>
);
};
const copyToClipboard = async (text: string, key: string) => {
const success = await utilCopyToClipboard(text);
if (success) {
@@ -738,23 +770,13 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
/>
<Card>
<Text className="font-semibold text-gray-900 mb-3">Guardrails</Text>
{info.guardrails && info.guardrails.length > 0 ? (
<div className="flex flex-wrap gap-2">
{info.guardrails.map((guardrail: string, index: number) => (
<Badge key={index} color="blue">
{guardrail}
</Badge>
))}
</div>
) : (
<Text className="text-gray-500">No guardrails configured</Text>
)}
{info.metadata?.disable_global_guardrails && (
<div className="mt-3 pt-3 border-t border-gray-200">
<Badge color="yellow">Global Guardrails Disabled</Badge>
</div>
)}
<GuardrailSettingsView
globalGuardrailNames={globalGuardrailNames}
teamGuardrails={info.metadata?.guardrails || []}
optedOutGlobalGuardrails={info.metadata?.opted_out_global_guardrails || []}
killSwitchOn={initialKillSwitchOn}
variant="inline"
/>
</Card>
<Card>
@@ -839,10 +861,23 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
)}
</div>
{isEditing ? (
{isEditing && isGuardrailsLoading ? (
<div className="p-4">Loading...</div>
) : isEditing ? (
<Form
form={form}
onFinish={handleTeamUpdate}
onValuesChange={(changedValues) => {
if ("disable_global_guardrails" in changedValues) {
const checked = changedValues.disable_global_guardrails === true;
const current = (form.getFieldValue("guardrails") || []) as string[];
const nonGlobals = current.filter((n) => !globalGuardrailNames.has(n));
form.setFieldValue(
"guardrails",
checked ? nonGlobals : [...Array.from(globalGuardrailNames), ...nonGlobals],
);
}
}}
initialValues={{
...info,
team_alias: info.team_alias,
@@ -866,7 +901,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
team_member_rpm_limit: info.team_member_budget_table?.rpm_limit,
team_member_budget: info.team_member_budget_table?.max_budget,
team_member_budget_duration: info.team_member_budget_table?.budget_duration,
guardrails: info.metadata?.guardrails || [],
guardrails: effectiveGuardrails,
policies: info.policies || [],
disable_global_guardrails: info.metadata?.disable_global_guardrails || false,
soft_budget_alerting_emails:
@@ -1090,7 +1125,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
label={
<span>
Guardrails{" "}
<Tooltip title="Setup your first guardrail">
<Tooltip title="Select which guardrails apply to this team. Global guardrails are enabled by default — uncheck to opt out. Other guardrails are opt-in.">
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
target="_blank"
@@ -1103,27 +1138,61 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
</span>
}
name="guardrails"
help="Select existing guardrails or enter new ones"
>
<Select
mode="tags"
placeholder="Select or enter guardrails"
options={guardrailsList.map((name) => ({ value: name, label: name }))}
/>
mode="multiple"
placeholder="Select guardrails"
optionLabelProp="label"
tagRender={renderGuardrailTag}
>
<Select.OptGroup
label={
<>
<GlobalOutlined style={{ marginInlineEnd: 4 }} />
Global
</>
}
>
{(guardrailsData?.guardrails ?? [])
.filter((g) => g.litellm_params?.default_on)
.map((g) => (
<Select.Option
key={g.guardrail_name}
value={g.guardrail_name}
label={g.guardrail_name}
disabled={killSwitchOn}
>
{g.guardrail_name}
</Select.Option>
))}
</Select.OptGroup>
<Select.OptGroup label="Other">
{(guardrailsData?.guardrails ?? [])
.filter((g) => !g.litellm_params?.default_on)
.map((g) => (
<Select.Option
key={g.guardrail_name}
value={g.guardrail_name}
label={g.guardrail_name}
>
{g.guardrail_name}
</Select.Option>
))}
</Select.OptGroup>
</Select>
</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)">
Disable all global guardrails{" "}
<Tooltip title="Kill switch: bypass every global guardrail for this team, including any added in the future. For per-guardrail opt-out instead, use the Guardrails dropdown above.">
<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>
@@ -1145,7 +1214,6 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
</span>
}
name="policies"
help="Select existing policies or enter new ones"
>
<Select
mode="tags"
@@ -1382,17 +1450,6 @@ 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"
@@ -1400,6 +1457,15 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
accessToken={accessToken}
/>
<GuardrailSettingsView
globalGuardrailNames={globalGuardrailNames}
teamGuardrails={info.metadata?.guardrails || []}
optedOutGlobalGuardrails={info.metadata?.opted_out_global_guardrails || []}
killSwitchOn={initialKillSwitchOn}
variant="inline"
className="pt-4 border-t border-gray-200"
/>
<LoggingSettingsView
loggingConfigs={info.metadata?.logging || []}
disabledCallbacks={[]}