diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 1d3ef2e10c2..064538ef3bf 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -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 @@ -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()}, " @@ -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, ) diff --git a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py index b9485a2e4cb..da6a5aeab09 100644 --- a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py +++ b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py @@ -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 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx index 972c39d49d8..0aa42b69a04 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx @@ -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"; @@ -76,6 +76,7 @@ const CreateTeamModal = ({ const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState(null); const [modelsToPick, setModelsToPick] = useState([]); const [guardrailsList, setGuardrailsList] = useState([]); + const [policiesList, setPoliciesList] = useState([]); const [mcpAccessGroups, setMcpAccessGroups] = useState([]); const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); @@ -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) => { @@ -531,6 +547,36 @@ const CreateTeamModal = ({ unCheckedChildren="No" /> + + Policies{" "} + + e.stopPropagation()} + > + + + + + } + name="policies" + className="mt-8" + help="Select existing policies or enter new ones" + > + ({ + value: name, + label: name, + }))} + /> + diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 543d95d2ccd..f24f4d60219 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -107,7 +107,7 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse { key: "agents", page: "agents", - label: Agents, + label: "Agents", icon: , roles: rolesWithWriteAccess, }, @@ -127,7 +127,11 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse { key: "policies", page: "policies", - label: "Policies", + label: ( + + Policies + + ), icon: , roles: all_admin_roles, }, diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index ffc0f80911d..5407c85bf98 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -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`; diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index 6bfb3696606..1edbba28afc 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -29,6 +29,7 @@ import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions"; import NotificationsManager from "../molecules/notifications_manager"; import { getGuardrailsList, + getPoliciesList, getPossibleUserRoles, getPromptsList, keyCreateCall, @@ -150,6 +151,7 @@ const CreateKey: React.FC = ({ team, teams, data, addKey }) => { const [keyOwner, setKeyOwner] = useState("you"); const [predefinedTags, setPredefinedTags] = useState(getPredefinedTags(data)); const [guardrailsList, setGuardrailsList] = useState([]); + const [policiesList, setPoliciesList] = useState([]); const [promptsList, setPromptsList] = useState([]); const [loggingSettings, setLoggingSettings] = useState([]); const [selectedCreateKeyTeam, setSelectedCreateKeyTeam] = useState(team); @@ -211,6 +213,16 @@ const CreateKey: React.FC = ({ 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); @@ -221,6 +233,7 @@ const CreateKey: React.FC = ({ team, teams, data, addKey }) => { }; fetchGuardrails(); + fetchPolicies(); fetchPrompts(); }, [accessToken]); @@ -915,6 +928,42 @@ const CreateKey: React.FC = ({ team, teams, data, addKey }) => { > + + Policies{" "} + + e.stopPropagation()} // Prevent accordion from collapsing when clicking link + > + + + + + } + name="policies" + className="mt-4" + help={ + premiumUser + ? "Select existing policies or enter new ones" + : "Premium feature - Upgrade to set policies by key" + } + > + ({ + label: model, + value: model, + }))} tokenSeparators={[","]} + showSearch + filterOption={(input, option) => + (option?.label ?? "").toLowerCase().includes(input.toLowerCase()) + } style={{ width: "100%" }} /> diff --git a/ui/litellm-dashboard/src/components/policies/add_policy_form.tsx b/ui/litellm-dashboard/src/components/policies/add_policy_form.tsx index 0b4f83a1611..383e9c45dfb 100644 --- a/ui/litellm-dashboard/src/components/policies/add_policy_form.tsx +++ b/ui/litellm-dashboard/src/components/policies/add_policy_form.tsx @@ -344,6 +344,14 @@ const AddPolicyForm: React.FC = ({ Conditions (Optional) + + = ({ {modelConditionType === "model" ? ( ({ value: name, label: name }))} + /> + + form.setFieldValue("vector_stores", values)} diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx index 836ebb385f1..2d3eaf2bb7f 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx @@ -1,4 +1,5 @@ import GuardrailSelector from "@/components/guardrails/GuardrailSelector"; +import PolicySelector from "@/components/policies/PolicySelector"; import { InfoCircleOutlined } from "@ant-design/icons"; import { TextInput, Button as TremorButton } from "@tremor/react"; import { Form, Input, Select, Switch, Tooltip } from "antd"; @@ -406,6 +407,28 @@ export function KeyEditView({ + + Policies{" "} + + + + + } + name="policies" + > + {accessToken && ( + { + form.setFieldValue("policies", v); + }} + accessToken={accessToken} + disabled={!premiumUser} + /> + )} + +