From f9639654fac6c1b1f00330b569f760114a9ffab9 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Sat, 11 Apr 2026 15:20:45 -0700 Subject: [PATCH] feat(ui/team): per-guardrail opt-out for global guardrails Replace the team info page's tags-mode guardrails Select with a grouped multi-select that splits guardrails into "Global" (default_on=true) and "Other" sections. On submit, derive both metadata fields from the single selection array: - metadata.guardrails: non-global opt-ins (additive) - metadata.disabled_global_guardrails: globals the team has opted out of The legacy disable_global_guardrails kill-switch toggle stays alongside the multiselect and remains semantically distinct: it kills all global guardrails including any added in the future, while the new list lets new globals auto-apply unless explicitly excluded. Other changes: - Switch from a manual useEffect/useState guardrails fetch to the consolidated useGuardrails hook - Replace stale "Select existing guardrails or enter new ones" help text with a single tooltip on each section's info icon - Render selected chips with green/blue color coding via tagRender so global vs non-global is visible without opening the dropdown - Drop the redundant [Global] tag from inside the OptGroup options (the group label already conveys it) - Update the read-only display to show the effective set (globals not opted out + additive opt-ins) with [Global] suffix on globals --- .../src/components/team/TeamInfo.tsx | 103 ++++++++++++------ 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx index 6308be65860..edc95aff21b 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx @@ -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 { 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"; @@ -179,7 +179,8 @@ const TeamInfoView: React.FC = ({ const [mcpAccessGroups, setMcpAccessGroups] = useState([]); const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); const [copiedStates, setCopiedStates] = useState>({}); - const [guardrailsList, setGuardrailsList] = useState([]); + const { data: guardrailsData } = useGuardrails(); + const globalGuardrailNames = guardrailsData?.globalGuardrailNames ?? new Set(); const [policiesList, setPoliciesList] = useState([]); const [policyGuardrails, setPolicyGuardrails] = useState>({}); const [loadingPolicies, setLoadingPolicies] = useState(false); @@ -273,17 +274,6 @@ const TeamInfoView: React.FC = ({ }; 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 +285,6 @@ const TeamInfoView: React.FC = ({ } }; - fetchGuardrails(); fetchPolicies(); }, [accessToken]); @@ -504,7 +493,10 @@ const TeamInfoView: React.FC = ({ budget_duration: values.budget_duration, metadata: { ...parsedMetadata, - ...(values.guardrails?.length > 0 ? { guardrails: values.guardrails } : {}), + guardrails: (values.guardrails || []).filter((n: string) => !globalGuardrailNames.has(n)), + disabled_global_guardrails: Array.from(globalGuardrailNames).filter( + (n) => !(values.guardrails || []).includes(n), + ), ...(values.logging_settings?.length > 0 ? { logging: values.logging_settings } : {}), disable_global_guardrails: values.disable_global_guardrails || false, soft_budget_alerting_emails: @@ -610,6 +602,29 @@ const TeamInfoView: React.FC = ({ const { team_info: info } = teamData; + const optedOutGlobals = new Set(info.metadata?.disabled_global_guardrails || []); + const effectiveGuardrails = [ + ...Array.from(globalGuardrailNames).filter((n) => !optedOutGlobals.has(n)), + ...(info.metadata?.guardrails || []), + ]; + + const preventTagMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const renderGuardrailTag = ({ label, value, closable, onClose }: any) => ( + + {label} + + ); + const copyToClipboard = async (text: string, key: string) => { const success = await utilCopyToClipboard(text); if (success) { @@ -739,11 +754,12 @@ const TeamInfoView: React.FC = ({ Guardrails - {info.guardrails && info.guardrails.length > 0 ? ( + {effectiveGuardrails.length > 0 ? (
- {info.guardrails.map((guardrail: string, index: number) => ( - - {guardrail} + {effectiveGuardrails.map((name) => ( + + {name} + {globalGuardrailNames.has(name) ? " · Global" : ""} ))}
@@ -866,7 +882,7 @@ const TeamInfoView: React.FC = ({ 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 +1106,7 @@ const TeamInfoView: React.FC = ({ label={ Guardrails{" "} - + = ({ } name="guardrails" - help="Select existing guardrails or enter new ones" > - Disable Global Guardrails - + Disable all global guardrails{" "} + } name="disable_global_guardrails" valuePropName="checked" - help="Bypass global guardrails for this team" > @@ -1145,7 +1187,6 @@ const TeamInfoView: React.FC = ({ } name="policies" - help="Select existing policies or enter new ones" >