Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions litellm/integrations/custom_guardrail.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,13 +259,24 @@ 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"]
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
Comment on lines +262 to 267

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Breaking rename of disable_global_guardrail to disable_global_guardrails

get_disable_global_guardrail now exclusively checks for the plural key disable_global_guardrails, dropping the old singular form. Any caller who was passing disable_global_guardrail: true (singular) directly in request data or litellm_metadata/metadata will silently find the kill switch no longer working after this deploy. The old test cases were explicitly exercising this direct-request path with the singular key, confirming it was the supported API contract.

The team-metadata path was already writing the plural form (via litellm_pre_call_utils.py), so there's a legitimate fix here — but it should be additive rather than a hard rename:

def get_disable_global_guardrail(self, data: dict) -> Optional[bool]:
    """Returns True if the global guardrail should be disabled"""
    # Check plural (new) then singular (backwards compat)
    if "disable_global_guardrails" in data:
        return data["disable_global_guardrails"]
    if "disable_global_guardrail" in data:  # legacy key
        return data["disable_global_guardrail"]
    metadata = data.get("litellm_metadata") or data.get("metadata", {})
    if "disable_global_guardrails" in metadata:
        return metadata["disable_global_guardrails"]
    if "disable_global_guardrail" in metadata:  # legacy key
        return metadata["disable_global_guardrail"]
    return False

Rule Used: What: avoid backwards-incompatible changes without... (source)


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.
Expand Down Expand Up @@ -406,6 +417,7 @@ def should_run_guardrail(
"""
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,
Expand All @@ -414,6 +426,9 @@ def should_run_guardrail(
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):
Expand Down
6 changes: 6 additions & 0 deletions litellm/proxy/litellm_pre_call_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down
123 changes: 119 additions & 4 deletions tests/test_litellm/integrations/test_custom_guardrail.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def test_should_run_guardrail_with_disable_global_guardrail(self):
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
Expand All @@ -210,7 +210,7 @@ def test_should_run_guardrail_with_disable_global_guardrail(self):
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
Expand All @@ -223,7 +223,7 @@ def test_should_run_guardrail_with_disable_global_guardrail(self):
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
Expand All @@ -236,7 +236,7 @@ def test_should_run_guardrail_with_disable_global_guardrail(self):
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
Expand All @@ -245,6 +245,121 @@ def test_should_run_guardrail_with_disable_global_guardrail(self):
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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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");
});

Expand Down Expand Up @@ -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"]),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
},
});
};
Loading
Loading