mirror of
https://github.com/tiennm99/litellm.git
synced 2026-06-27 23:06:50 +00:00
[Feat] UI + Backend - Allow adding policies on Keys/Teams + Viewing on Info panels (#19688)
* ui for policy mgmt * test_add_guardrails_from_policy_engine_accepts_dynamic_policies_and_pops_from_data
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+47
-1
@@ -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<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);
|
||||
|
||||
@@ -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>) => {
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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<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);
|
||||
@@ -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);
|
||||
@@ -221,6 +233,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
|
||||
};
|
||||
|
||||
fetchGuardrails();
|
||||
fetchPolicies();
|
||||
fetchPrompts();
|
||||
}, [accessToken]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { Modal, Form, Select, Radio, Divider, Typography } from "antd";
|
||||
import { Button } from "@tremor/react";
|
||||
import { Policy, PolicyAttachmentCreateRequest } from "./types";
|
||||
import { teamListCall, keyInfoCall } from "../networking";
|
||||
import { teamListCall, keyInfoCall, modelAvailableCall } from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
@@ -30,24 +30,27 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
||||
const [scopeType, setScopeType] = useState<"global" | "specific">("global");
|
||||
const [availableTeams, setAvailableTeams] = useState<string[]>([]);
|
||||
const [availableKeys, setAvailableKeys] = useState<string[]>([]);
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const [isLoadingTeams, setIsLoadingTeams] = useState(false);
|
||||
const [isLoadingKeys, setIsLoadingKeys] = useState(false);
|
||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
||||
const { userId, userRole } = useAuthorized();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && accessToken) {
|
||||
loadTeamsAndKeys();
|
||||
loadTeamsKeysAndModels();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, accessToken]);
|
||||
|
||||
const loadTeamsAndKeys = async () => {
|
||||
const loadTeamsKeysAndModels = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
// Load teams
|
||||
setIsLoadingTeams(true);
|
||||
try {
|
||||
const teamsResponse = await teamListCall(accessToken, userId, userRole);
|
||||
// Pass null for organizationID since we're loading all teams the user has access to
|
||||
const teamsResponse = await teamListCall(accessToken, null, userId);
|
||||
if (teamsResponse?.data) {
|
||||
const teamAliases = teamsResponse.data
|
||||
.map((t: any) => t.team_alias)
|
||||
@@ -75,6 +78,22 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
||||
} finally {
|
||||
setIsLoadingKeys(false);
|
||||
}
|
||||
|
||||
// Load models
|
||||
setIsLoadingModels(true);
|
||||
try {
|
||||
const modelsResponse = await modelAvailableCall(accessToken, userId || "", userRole || "");
|
||||
if (modelsResponse?.data) {
|
||||
const modelIds = modelsResponse.data
|
||||
.map((m: any) => m.id || m.model_name)
|
||||
.filter(Boolean);
|
||||
setAvailableModels(modelIds);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load models:", error);
|
||||
} finally {
|
||||
setIsLoadingModels(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
@@ -230,12 +249,21 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
||||
<Form.Item
|
||||
name="models"
|
||||
label="Models"
|
||||
tooltip="Model names this attachment applies to. Supports wildcards (e.g., gpt-4*)"
|
||||
tooltip="Model names this attachment applies to. Supports wildcards (e.g., gpt-4*). Leave empty to apply to all models."
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="Enter model names (e.g., gpt-4, bedrock/*)"
|
||||
placeholder={isLoadingModels ? "Loading models..." : "Select or enter model names (e.g., gpt-4, bedrock/*)"}
|
||||
loading={isLoadingModels}
|
||||
options={availableModels.map((model) => ({
|
||||
label: model,
|
||||
value: model,
|
||||
}))}
|
||||
tokenSeparators={[","]}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -344,6 +344,14 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
||||
<Text strong>Conditions (Optional)</Text>
|
||||
</Divider>
|
||||
|
||||
<Alert
|
||||
message="Model Scope"
|
||||
description="By default, this policy will run on all models. You can optionally restrict it to specific models below."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<Form.Item label="Model Condition Type">
|
||||
<Radio.Group
|
||||
value={modelConditionType}
|
||||
@@ -359,18 +367,18 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
||||
|
||||
<Form.Item
|
||||
name="model_condition"
|
||||
label={modelConditionType === "model" ? "Model" : "Regex Pattern"}
|
||||
label={modelConditionType === "model" ? "Model (Optional)" : "Regex Pattern (Optional)"}
|
||||
tooltip={
|
||||
modelConditionType === "model"
|
||||
? "Select a specific model to apply this policy to"
|
||||
: "Enter a regex pattern to match models (e.g., gpt-4.* or bedrock/.*)"
|
||||
? "Select a specific model to apply this policy to. Leave empty to apply to all models."
|
||||
: "Enter a regex pattern to match models (e.g., gpt-4.* or bedrock/.*). Leave empty to apply to all models."
|
||||
}
|
||||
>
|
||||
{modelConditionType === "model" ? (
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="Select a model"
|
||||
placeholder="Leave empty to apply to all models"
|
||||
options={availableModels.map((model) => ({
|
||||
label: model,
|
||||
value: model,
|
||||
@@ -381,7 +389,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
) : (
|
||||
<TextInput placeholder="e.g., gpt-4.* or bedrock/claude-.*" />
|
||||
<TextInput placeholder="Leave empty to apply to all models (e.g., gpt-4.* or bedrock/claude-.*)" />
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
|
||||
import { Modal, message } from "antd";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import { Modal, message, Alert } from "antd";
|
||||
import { ExclamationCircleOutlined, InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { isAdminRole } from "@/utils/roles";
|
||||
import PolicyTable from "./policy_table";
|
||||
import PolicyInfoView from "./policy_info";
|
||||
@@ -181,6 +181,36 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Alert
|
||||
message="About Policies"
|
||||
description={
|
||||
<div>
|
||||
<p className="mb-3">
|
||||
Use policies to group guardrails and control which ones run for specific teams, keys, or models.
|
||||
</p>
|
||||
<p className="mb-2 font-semibold">Why use policies?</p>
|
||||
<ul className="list-disc list-inside mb-3 space-y-1 ml-2">
|
||||
<li>Enable/disable specific guardrails for teams, keys, or models</li>
|
||||
<li>Group guardrails into a single policy</li>
|
||||
<li>Inherit from existing policies and override what you need</li>
|
||||
</ul>
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 underline inline-block mt-1"
|
||||
>
|
||||
Learn more in the documentation →
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
icon={<InfoCircleOutlined />}
|
||||
showIcon
|
||||
closable
|
||||
className="mb-6"
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Button onClick={handleAddPolicy} disabled={!accessToken}>
|
||||
+ Add New Policy
|
||||
@@ -244,6 +274,37 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<Alert
|
||||
message="About Policy Attachments"
|
||||
description={
|
||||
<div>
|
||||
<p className="mb-3">
|
||||
Policy attachments control where your policies apply. Policies don't do anything until you attach them to specific teams, keys, models, or globally.
|
||||
</p>
|
||||
<p className="mb-2 font-semibold">Attachment Scopes:</p>
|
||||
<ul className="list-disc list-inside mb-3 space-y-1 ml-2">
|
||||
<li><strong>Global (*)</strong> - Applies to all requests</li>
|
||||
<li><strong>Teams</strong> - Applies only to specific teams</li>
|
||||
<li><strong>Keys</strong> - Applies only to specific API keys (supports wildcards like dev-*)</li>
|
||||
<li><strong>Models</strong> - Applies only when specific models are used</li>
|
||||
</ul>
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies#attachments"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 underline inline-block mt-1"
|
||||
>
|
||||
Learn more about attachments →
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
icon={<InfoCircleOutlined />}
|
||||
showIcon
|
||||
closable
|
||||
className="mb-6"
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Button
|
||||
onClick={() => setIsAddAttachmentModalVisible(true)}
|
||||
|
||||
@@ -2,6 +2,8 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import UserSearchModal from "@/components/common_components/user_search_modal";
|
||||
import {
|
||||
getGuardrailsList,
|
||||
getPoliciesList,
|
||||
getPolicyInfoWithGuardrails,
|
||||
Member,
|
||||
Organization,
|
||||
organizationInfoCall,
|
||||
@@ -171,6 +173,9 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
|
||||
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
|
||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||
const [policiesList, setPoliciesList] = useState<string[]>([]);
|
||||
const [policyGuardrails, setPolicyGuardrails] = useState<Record<string, string[]>>({});
|
||||
const [loadingPolicies, setLoadingPolicies] = useState(false);
|
||||
const [memberToDelete, setMemberToDelete] = useState<Member | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
@@ -247,9 +252,54 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPolicies = async () => {
|
||||
try {
|
||||
if (!accessToken) 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]);
|
||||
|
||||
// Fetch resolved guardrails for all policies
|
||||
useEffect(() => {
|
||||
const fetchPolicyGuardrails = async () => {
|
||||
if (!accessToken || !info?.policies || info.policies.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingPolicies(true);
|
||||
const guardrailsMap: Record<string, string[]> = {};
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
info.policies.map(async (policyName: string) => {
|
||||
try {
|
||||
const policyInfo = await getPolicyInfoWithGuardrails(accessToken, policyName);
|
||||
guardrailsMap[policyName] = policyInfo.resolved_guardrails || [];
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch guardrails for policy ${policyName}:`, error);
|
||||
guardrailsMap[policyName] = [];
|
||||
}
|
||||
})
|
||||
);
|
||||
setPolicyGuardrails(guardrailsMap);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch policy guardrails:", error);
|
||||
} finally {
|
||||
setLoadingPolicies(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPolicyGuardrails();
|
||||
}, [accessToken, info?.policies]);
|
||||
|
||||
const handleMemberCreate = async (values: any) => {
|
||||
try {
|
||||
if (accessToken == null) return;
|
||||
@@ -603,6 +653,56 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text className="font-semibold text-gray-900 mb-3">Policies</Text>
|
||||
{info.policies && info.policies.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{info.policies.map((policy: string, index: number) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge color="purple">{policy}</Badge>
|
||||
{loadingPolicies && <Text className="text-xs text-gray-400">Loading guardrails...</Text>}
|
||||
</div>
|
||||
{!loadingPolicies && policyGuardrails[policy] && policyGuardrails[policy].length > 0 && (
|
||||
<div className="ml-4 pl-3 border-l-2 border-gray-200">
|
||||
<Text className="text-xs text-gray-500 mb-1">Resolved Guardrails:</Text>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{policyGuardrails[policy].map((guardrail: string, gIndex: number) => (
|
||||
<Badge key={gIndex} color="blue" size="xs">
|
||||
{guardrail}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text className="text-gray-500">No policies configured</Text>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<LoggingSettingsView
|
||||
loggingConfigs={info.metadata?.logging || []}
|
||||
disabledCallbacks={[]}
|
||||
@@ -814,6 +914,32 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||
<Switch checkedChildren="Yes" 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"
|
||||
help="Select existing policies or enter new ones"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="Select or enter policies"
|
||||
options={policiesList.map((name) => ({ value: name, label: name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Vector Stores" name="vector_stores" aria-label="Vector Stores">
|
||||
<VectorStoreSelector
|
||||
onChange={(values: string[]) => form.setFieldValue("vector_stores", values)}
|
||||
|
||||
@@ -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({
|
||||
<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">
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="policies"
|
||||
>
|
||||
{accessToken && (
|
||||
<PolicySelector
|
||||
onChange={(v) => {
|
||||
form.setFieldValue("policies", v);
|
||||
}}
|
||||
accessToken={accessToken}
|
||||
disabled={!premiumUser}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Tags" name="tags">
|
||||
<Select
|
||||
mode="tags"
|
||||
|
||||
@@ -14,7 +14,7 @@ import { extractLoggingSettings, formatMetadataForDisplay, stripTagsFromMetadata
|
||||
import { KeyResponse } from "../key_team_helpers/key_list";
|
||||
import LoggingSettingsView from "../logging_settings_view";
|
||||
import NotificationManager from "../molecules/notifications_manager";
|
||||
import { keyDeleteCall, keyUpdateCall } from "../networking";
|
||||
import { keyDeleteCall, keyUpdateCall, getPolicyInfoWithGuardrails } from "../networking";
|
||||
import ObjectPermissionsView from "../object_permissions_view";
|
||||
import { RegenerateKeyModal } from "../organisms/regenerate_key_modal";
|
||||
import { parseErrorMessage } from "../shared/errorUtils";
|
||||
@@ -60,6 +60,8 @@ export default function KeyInfoView({
|
||||
const [currentKeyData, setCurrentKeyData] = useState<KeyResponse | undefined>(keyData);
|
||||
const [lastRegeneratedAt, setLastRegeneratedAt] = useState<Date | null>(null);
|
||||
const [isRecentlyRegenerated, setIsRecentlyRegenerated] = useState(false);
|
||||
const [policyGuardrails, setPolicyGuardrails] = useState<Record<string, string[]>>({});
|
||||
const [loadingPolicies, setLoadingPolicies] = useState(false);
|
||||
|
||||
// Update local state when keyData prop changes (but don't reset to undefined)
|
||||
useEffect(() => {
|
||||
@@ -68,6 +70,39 @@ export default function KeyInfoView({
|
||||
}
|
||||
}, [keyData]);
|
||||
|
||||
// Fetch resolved guardrails for all policies
|
||||
useEffect(() => {
|
||||
const fetchPolicyGuardrails = async () => {
|
||||
if (!accessToken || !currentKeyData?.metadata?.policies || currentKeyData.metadata.policies.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingPolicies(true);
|
||||
const guardrailsMap: Record<string, string[]> = {};
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
currentKeyData.metadata.policies.map(async (policyName: string) => {
|
||||
try {
|
||||
const policyInfo = await getPolicyInfoWithGuardrails(accessToken, policyName);
|
||||
guardrailsMap[policyName] = policyInfo.resolved_guardrails || [];
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch guardrails for policy ${policyName}:`, error);
|
||||
guardrailsMap[policyName] = [];
|
||||
}
|
||||
})
|
||||
);
|
||||
setPolicyGuardrails(guardrailsMap);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch policy guardrails:", error);
|
||||
} finally {
|
||||
setLoadingPolicies(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPolicyGuardrails();
|
||||
}, [accessToken, currentKeyData?.metadata?.policies]);
|
||||
|
||||
// Reset recent regeneration indicator after 5 seconds
|
||||
useEffect(() => {
|
||||
if (isRecentlyRegenerated) {
|
||||
@@ -484,6 +519,56 @@ export default function KeyInfoView({
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text className="font-medium mb-3">Guardrails</Text>
|
||||
{currentKeyData.guardrails && currentKeyData.guardrails.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{currentKeyData.guardrails.map((guardrail, index) => (
|
||||
<Badge key={index} color="blue">
|
||||
{guardrail}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text className="text-gray-500">No guardrails configured</Text>
|
||||
)}
|
||||
{currentKeyData.metadata?.disable_global_guardrails && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||
<Badge color="yellow">Global Guardrails Disabled</Badge>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text className="font-medium mb-3">Policies</Text>
|
||||
{currentKeyData.metadata?.policies && currentKeyData.metadata.policies.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{currentKeyData.metadata.policies.map((policy: string, index: number) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge color="purple">{policy}</Badge>
|
||||
{loadingPolicies && <Text className="text-xs text-gray-400">Loading guardrails...</Text>}
|
||||
</div>
|
||||
{!loadingPolicies && policyGuardrails[policy] && policyGuardrails[policy].length > 0 && (
|
||||
<div className="ml-4 pl-3 border-l-2 border-gray-200">
|
||||
<Text className="text-xs text-gray-500 mb-1">Resolved Guardrails:</Text>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{policyGuardrails[policy].map((guardrail: string, gIndex: number) => (
|
||||
<Badge key={gIndex} color="blue" size="xs">
|
||||
{guardrail}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text className="text-gray-500">No policies configured</Text>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<LoggingSettingsView
|
||||
loggingConfigs={extractLoggingSettings(currentKeyData.metadata)}
|
||||
disabledCallbacks={
|
||||
|
||||
Reference in New Issue
Block a user