Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
25 changes: 19 additions & 6 deletions litellm/proxy/litellm_pre_call_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1500,10 +1500,13 @@ def add_guardrails_from_policy_engine(
Add guardrails from the policy engine based on request context.

This function:
1. Gets matching policies based on team_alias, key_alias, and model
2. Resolves guardrails from matching policies (including inheritance)
3. Adds guardrails to request metadata
4. Tracks applied policies in metadata for response headers
1. Extracts "policies" from request body (if present) for dynamic policy application
2. Gets matching policies based on team_alias, key_alias, and model (via attachments)
3. Combines dynamic policies with attachment-based policies
4. Resolves guardrails from all policies (including inheritance)
5. Adds guardrails to request metadata
6. Tracks applied policies in metadata for response headers
7. Removes "policies" from request body so it's not forwarded to LLM provider

Args:
data: The request data to update
Expand All @@ -1519,6 +1522,10 @@ def add_guardrails_from_policy_engine(
from litellm.proxy.policy_engine.policy_resolver import PolicyResolver
from litellm.types.proxy.policy_engine import PolicyMatchContext

# Extract dynamic policies from request body (if present)
# These will be combined with attachment-based policies
request_body_policies = data.pop("policies", None)

registry = get_policy_registry()
verbose_proxy_logger.debug(
f"Policy engine: registry initialized={registry.is_initialized()}, "
Expand All @@ -1545,12 +1552,18 @@ def add_guardrails_from_policy_engine(

verbose_proxy_logger.debug(f"Policy engine: matched policies via attachments: {matching_policy_names}")

if not matching_policy_names:
# Combine attachment-based policies with dynamic request body policies
all_policy_names = set(matching_policy_names)
if request_body_policies and isinstance(request_body_policies, list):
all_policy_names.update(request_body_policies)
verbose_proxy_logger.debug(f"Policy engine: added dynamic policies from request body: {request_body_policies}")

if not all_policy_names:
return

# Filter to only policies whose conditions match the context
applied_policy_names = PolicyMatcher.get_policies_with_matching_conditions(
policy_names=matching_policy_names,
policy_names=list(all_policy_names),
context=context,
)

Expand Down
47 changes: 47 additions & 0 deletions tests/test_litellm/proxy/test_litellm_pre_call_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1548,3 +1548,50 @@ def test_add_guardrails_from_policy_engine():
policy_registry._initialized = False
attachment_registry._attachments = []
attachment_registry._initialized = False


def test_add_guardrails_from_policy_engine_accepts_dynamic_policies_and_pops_from_data():
"""
Test that add_guardrails_from_policy_engine accepts dynamic 'policies' from the request body
and removes them to prevent forwarding to the LLM provider.

This is critical because 'policies' is a LiteLLM proxy-specific parameter that should
not be sent to the actual LLM API (e.g., OpenAI, Anthropic, etc.).
"""
from litellm.proxy.policy_engine.policy_registry import get_policy_registry

# Setup test data with 'policies' in the request body
data = {
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello"}],
"policies": ["PII-POLICY-GLOBAL", "HIPAA-POLICY"], # Dynamic policies - should be accepted and removed
"metadata": {},
}

user_api_key_dict = UserAPIKeyAuth(
api_key="test-key",
team_alias="test-team",
key_alias="test-key",
)

# Initialize empty policy registry (we're just testing the accept and pop behavior)
policy_registry = get_policy_registry()
policy_registry._policies = {}
policy_registry._initialized = False

# Call the function - should accept dynamic policies and not raise an error
add_guardrails_from_policy_engine(
data=data,
metadata_variable_name="metadata",
user_api_key_dict=user_api_key_dict,
)

# Verify that 'policies' was removed from the request body
assert "policies" not in data, "'policies' should be removed from request body to prevent forwarding to LLM provider"

# Verify that other fields are preserved
assert "model" in data
assert data["model"] == "gpt-4"
assert "messages" in data
assert data["messages"] == [{"role": "user", "content": "Hello"}]
assert "metadata" in data
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import PremiumLoggingSettings from "@/components/common_components/PremiumLoggin
import ModelAliasManager from "@/components/common_components/ModelAliasManager";
import React, { useEffect, useState } from "react";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { fetchMCPAccessGroups, getGuardrailsList, Organization, Team, teamCreateCall } from "@/components/networking";
import { fetchMCPAccessGroups, getGuardrailsList, getPoliciesList, Organization, Team, teamCreateCall } from "@/components/networking";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import MCPToolPermissions from "@/components/mcp_server_management/MCPToolPermissions";

Expand Down Expand Up @@ -76,6 +76,7 @@ const CreateTeamModal = ({
const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState<Organization | null>(null);
const [modelsToPick, setModelsToPick] = useState<string[]>([]);
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
const [policiesList, setPoliciesList] = useState<string[]>([]);
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);

Expand Down Expand Up @@ -136,7 +137,22 @@ const CreateTeamModal = ({
}
};

const fetchPolicies = async () => {
try {
if (accessToken == null) {
return;
}

const response = await getPoliciesList(accessToken);
const policyNames = response.policies.map((p: { policy_name: string }) => p.policy_name);
setPoliciesList(policyNames);
} catch (error) {
console.error("Failed to fetch policies:", error);
}
};

fetchGuardrails();
fetchPolicies();
}, [accessToken]);

const handleCreate = async (formValues: Record<string, any>) => {
Expand Down Expand Up @@ -531,6 +547,36 @@ const CreateTeamModal = ({
unCheckedChildren="No"
/>
</Form.Item>
<Form.Item
label={
<span>
Policies{" "}
<Tooltip title="Apply policies to this team to control guardrails and other settings">
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</span>
}
name="policies"
className="mt-8"
help="Select existing policies or enter new ones"
>
<Select2
mode="tags"
style={{ width: "100%" }}
placeholder="Select or enter policies"
options={policiesList.map((name) => ({
value: name,
label: name,
}))}
/>
</Form.Item>
<Form.Item
label={
<span>
Expand Down
8 changes: 6 additions & 2 deletions ui/litellm-dashboard/src/components/leftnav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
{
key: "agents",
page: "agents",
label: <span className="flex items-center gap-4">Agents</span>,
label: "Agents",
icon: <RobotOutlined />,
roles: rolesWithWriteAccess,
},
Expand All @@ -127,7 +127,11 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
{
key: "policies",
page: "policies",
label: "Policies",
label: (
<span className="flex items-center gap-4">
Policies <NewBadge />
</span>
),
icon: <AuditOutlined />,
roles: all_admin_roles,
},
Expand Down
26 changes: 26 additions & 0 deletions ui/litellm-dashboard/src/components/networking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5377,6 +5377,32 @@ export const getPoliciesList = async (accessToken: string) => {
}
};

export const getPolicyInfoWithGuardrails = async (accessToken: string, policyName: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policy/info/${policyName}` : `/policy/info/${policyName}`;
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});

if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}

const data = await response.json();
return data;
} catch (error) {
console.error(`Failed to get policy info for ${policyName}:`, error);
throw error;
}
};

export const createPolicyCall = async (accessToken: string, policyData: any) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies` : `/policies`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
import NotificationsManager from "../molecules/notifications_manager";
import {
getGuardrailsList,
getPoliciesList,
getPossibleUserRoles,
getPromptsList,
keyCreateCall,
Expand Down Expand Up @@ -150,6 +151,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
const [keyOwner, setKeyOwner] = useState("you");
const [predefinedTags, setPredefinedTags] = useState(getPredefinedTags(data));
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
const [policiesList, setPoliciesList] = useState<string[]>([]);
const [promptsList, setPromptsList] = useState<string[]>([]);
const [loggingSettings, setLoggingSettings] = useState<any[]>([]);
const [selectedCreateKeyTeam, setSelectedCreateKeyTeam] = useState<Team | null>(team);
Expand Down Expand Up @@ -211,6 +213,16 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
}
};

const fetchPolicies = async () => {
try {
const response = await getPoliciesList(accessToken);
const policyNames = response.policies.map((p: { policy_name: string }) => p.policy_name);
setPoliciesList(policyNames);
} catch (error) {
console.error("Failed to fetch policies:", error);
}
};

const fetchPrompts = async () => {
try {
const response = await getPromptsList(accessToken);
Expand All @@ -221,6 +233,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
};

fetchGuardrails();
fetchPolicies();
fetchPrompts();
}, [accessToken]);

Expand Down Expand Up @@ -915,6 +928,42 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
>
<Switch disabled={!premiumUser} checkedChildren="Yes" unCheckedChildren="No" />
</Form.Item>
<Form.Item
label={
<span>
Policies{" "}
<Tooltip title="Apply policies to this key to control guardrails and other settings">
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</span>
}
name="policies"
className="mt-4"
help={
premiumUser
? "Select existing policies or enter new ones"
: "Premium feature - Upgrade to set policies by key"
}
>
<Select
mode="tags"
style={{ width: "100%" }}
disabled={!premiumUser}
placeholder={
!premiumUser
? "Premium feature - Upgrade to set policies by key"
: "Select or enter policies"
}
options={policiesList.map((name) => ({ value: name, label: name }))}
/>
</Form.Item>
<Form.Item
label={
<span>
Expand Down
Loading
Loading