diff --git a/AGENTS.md b/AGENTS.md index 37411938e2..9493a3404a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,9 @@ LiteLLM is a unified interface for 100+ LLMs that: ### MAKING CODE CHANGES FOR THE UI (IGNORE FOR BACKEND) -1. **Tremor is DEPRECATED, do not use Tremor components in new features/changes** +1. **Always use `antd` for new UI components — Tremor is DEPRECATED** + - We are migrating off of `@tremor/react`. Do not introduce new `Badge`, `Text`, `Card`, `Grid`, `Title`, or other imports from `@tremor/react` in any new or modified file. + - Use `antd` equivalents: `Tag` for labels, plain ``/`
` with Tailwind classes (or `Typography.Text`) for text, `Card` from `antd`, etc. Note that `antd` has no `"yellow"` Tag color — use `"gold"` for amber/yellow. - The only exception is the Tremor Table component and its required Tremor Table sub components. 2. **Use Common Components as much as possible**: diff --git a/CLAUDE.md b/CLAUDE.md index a8800ff888..a4e723d5b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,6 +109,9 @@ LiteLLM is a unified interface for 100+ LLM providers with two main components: ### UI / Backend Consistency - When wiring a new UI entity type to an existing backend endpoint, verify the backend API contract (single value vs. array, required vs. optional params) and ensure the UI controls match — e.g., use a single-select dropdown when the backend accepts a single value, not a multi-select +### UI Component Library +- **Always use `antd` for new UI components** — we are migrating off of `@tremor/react`. Do not introduce new `Badge`, `Text`, `Card`, `Grid`, `Title`, or other imports from `@tremor/react` in any new or modified file. Use `antd` equivalents: `Tag` for labels, plain ``/`
` with Tailwind classes (or `Typography.Text`) for text, `Card` from `antd`, etc. Note that `antd` has no `"yellow"` Tag color — use `"gold"` for amber/yellow. + ### MCP OAuth / OpenAPI Transport Mapping - `TRANSPORT.OPENAPI` is a UI-only concept. The backend only accepts `"http"`, `"sse"`, or `"stdio"`. Always map it to `"http"` before any API call (including pre-OAuth temp-session calls). - FastAPI validation errors return `detail` as an array of `{loc, msg, type}` objects. Error extractors must handle: array (map `.msg`), string, nested `{error: string}`, and fallback. diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index aa2a8121ee..6046f1bb58 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -259,13 +259,24 @@ class CustomGuardrail(CustomLogger): """ 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 + 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. @@ -406,6 +417,7 @@ class CustomGuardrail(CustomLogger): """ 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, @@ -414,6 +426,9 @@ class CustomGuardrail(CustomLogger): 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): diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 2b8c16ed12..4ec31925ea 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -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 ): diff --git a/tests/test_litellm/integrations/test_custom_guardrail.py b/tests/test_litellm/integrations/test_custom_guardrail.py index 7c60cbb52e..3a959d599b 100644 --- a/tests/test_litellm/integrations/test_custom_guardrail.py +++ b/tests/test_litellm/integrations/test_custom_guardrail.py @@ -197,7 +197,7 @@ class TestCustomGuardrailShouldRunGuardrail: 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 @@ -210,7 +210,7 @@ class TestCustomGuardrailShouldRunGuardrail: 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 @@ -223,7 +223,7 @@ class TestCustomGuardrailShouldRunGuardrail: 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 @@ -236,7 +236,7 @@ class TestCustomGuardrailShouldRunGuardrail: 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 @@ -245,6 +245,121 @@ class TestCustomGuardrailShouldRunGuardrail: 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): diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_panw_prisma_airs.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_panw_prisma_airs.py index 7486f602dd..ace3901b37 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_panw_prisma_airs.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_panw_prisma_airs.py @@ -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", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.test.ts index d9e96a5308..b1896eda0e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.test.ts @@ -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); @@ -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"); }); @@ -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"]), + ); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.ts index 9786b7fa35..5c7a8df050 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.ts @@ -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 | null; + guardrail_id?: string | null; + [key: string]: unknown; +} + +interface GuardrailsListResponse { + guardrails: GuardrailListItem[]; +} + +export interface GuardrailsListData { + guardrails: GuardrailListItem[]; + globalGuardrailNames: Set; + optionalGuardrailNames: Set; +} + +// ── Hook ───────────────────────────────────────────────────────────────────── + const guardrailKeys = createQueryKeys("guardrails"); -export const useGuardrails = (): UseQueryResult => { +export const useGuardrails = (): UseQueryResult => { const { accessToken, userId, userRole } = useAuthorized(); - return useQuery({ + return useQuery({ 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(); + const optionalGuardrailNames = new Set(); + 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 }; + }, }); }; diff --git a/ui/litellm-dashboard/src/components/GuardrailSettingsView.tsx b/ui/litellm-dashboard/src/components/GuardrailSettingsView.tsx new file mode 100644 index 0000000000..e322ce2978 --- /dev/null +++ b/ui/litellm-dashboard/src/components/GuardrailSettingsView.tsx @@ -0,0 +1,97 @@ +import React from "react"; +import { Tag } from "antd"; +import { GlobalOutlined } from "@ant-design/icons"; + +interface GuardrailSettingsViewProps { + globalGuardrailNames: Set; + teamGuardrails?: string[]; + optedOutGlobalGuardrails?: string[]; + killSwitchOn?: boolean; + variant?: "card" | "inline"; + className?: string; +} + +export function GuardrailSettingsView({ + globalGuardrailNames, + teamGuardrails = [], + optedOutGlobalGuardrails = [], + killSwitchOn = false, + variant = "card", + className = "", +}: GuardrailSettingsViewProps) { + const optedOutSet = new Set(optedOutGlobalGuardrails); + const globalsRunning = Array.from(globalGuardrailNames).filter( + (n) => !optedOutSet.has(n), + ); + const nonGlobalOptIns = teamGuardrails.filter( + (n) => !globalGuardrailNames.has(n), + ); + + const isEmpty = + !killSwitchOn && globalsRunning.length === 0 && nonGlobalOptIns.length === 0; + + const content = isEmpty ? ( + No guardrails configured + ) : ( +
+
+ + + Global + + {killSwitchOn ? ( + Bypassed for this team + ) : globalsRunning.length > 0 ? ( +
+ {globalsRunning.map((name) => ( + + {name} + + ))} +
+ ) : ( + None configured + )} +
+
+ Team-specific + {nonGlobalOptIns.length > 0 ? ( +
+ {nonGlobalOptIns.map((name) => ( + + {name} + + ))} +
+ ) : ( + None configured + )} +
+
+ ); + + if (variant === "card") { + return ( +
+
+
+ Guardrails Settings + + Global and team-specific guardrails applied to this team + +
+
+ {content} +
+ ); + } + + return ( +
+ Guardrails Settings + {content} +
+ ); +} + +export default GuardrailSettingsView; diff --git a/ui/litellm-dashboard/src/components/add_model/AddModelForm.test.tsx b/ui/litellm-dashboard/src/components/add_model/AddModelForm.test.tsx index de5e138739..4a3dbaedf7 100644 --- a/ui/litellm-dashboard/src/components/add_model/AddModelForm.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/AddModelForm.test.tsx @@ -82,7 +82,11 @@ vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ vi.mock("@/app/(dashboard)/hooks/guardrails/useGuardrails", () => ({ useGuardrails: vi.fn().mockReturnValue({ - data: [{ guardrail_name: "test-guardrail" }], + data: { + guardrails: [{ guardrail_name: "test-guardrail" }], + globalGuardrailNames: new Set(), + optionalGuardrailNames: new Set(["test-guardrail"]), + }, isLoading: false, error: null, }), diff --git a/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx b/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx index 65e239d58e..969373421c 100644 --- a/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx +++ b/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx @@ -63,7 +63,8 @@ const AddModelForm: React.FC = ({ isLoading: isProviderMetadataLoading, error: providerMetadataError, } = useProviderFields(); - const { data: guardrailsList, isLoading: isGuardrailsLoading, error: guardrailsError } = useGuardrails(); + const { data: guardrailsData } = useGuardrails(); + const guardrailsList = guardrailsData?.guardrails.map((g) => g.guardrail_name); const { data: tagsList, isLoading: isTagsLoading, error: tagsError } = useTags(); const handleTestConnection = async () => { diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx index f24b4e77e1..b5cc6005c9 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx @@ -276,15 +276,17 @@ describe("TeamInfoView", () => { it("should display guardrails in overview when present", async () => { vi.mocked(networking.teamInfoCall).mockResolvedValue( createMockTeamData({ - guardrails: ["guardrail1", "guardrail2"], + metadata: { guardrails: ["guardrail1", "guardrail2"] }, }) ); renderWithProviders(); await waitFor(() => { - expect(screen.getByText("Guardrails")).toBeInTheDocument(); + expect(screen.getByText("Guardrails Settings")).toBeInTheDocument(); }); + expect(screen.getByText("guardrail1")).toBeInTheDocument(); + expect(screen.getByText("guardrail2")).toBeInTheDocument(); }); it("should display policies in overview when present", async () => { diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx index 6308be6586..3a9794b692 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 { EditOutlined, GlobalOutlined, 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"; @@ -31,6 +31,7 @@ import DeleteResourceModal from "../common_components/DeleteResourceModal"; import DurationSelect from "../common_components/DurationSelect"; import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector"; import { unfurlWildcardModelsInList } from "../key_team_helpers/fetch_available_models_team_key"; +import GuardrailSettingsView from "../GuardrailSettingsView"; import LoggingSettingsView from "../logging_settings_view"; import MCPServerSelector from "../mcp_server_management/MCPServerSelector"; import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions"; @@ -179,7 +180,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, isLoading: isGuardrailsLoading } = useGuardrails(); + const globalGuardrailNames = guardrailsData?.globalGuardrailNames ?? new Set(); const [policiesList, setPoliciesList] = useState([]); const [policyGuardrails, setPolicyGuardrails] = useState>({}); const [loadingPolicies, setLoadingPolicies] = useState(false); @@ -202,6 +204,7 @@ const TeamInfoView: React.FC = ({ // Models currently selected in the team edit form, used to scope the per-model // rate limit dropdown to models this team actually has access to. const selectedModelsInForm = Form.useWatch("models", form) as string[] | undefined; + const killSwitchOn = Form.useWatch("disable_global_guardrails", form) as boolean | undefined; const availableRateLimitModels = useMemo(() => { const selected = selectedModelsInForm ?? teamData?.team_info?.models ?? []; if (selected.includes("all-proxy-models") || selected.includes("all-team-models")) { @@ -273,17 +276,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 +287,6 @@ const TeamInfoView: React.FC = ({ } }; - fetchGuardrails(); fetchPolicies(); }, [accessToken]); @@ -491,6 +482,13 @@ const TeamInfoView: React.FC = ({ } } + const killSwitchOnAtSave = values.disable_global_guardrails === true; + const optedOutGlobalGuardrails = killSwitchOnAtSave + ? Array.from(globalGuardrailNames) + : Array.from(globalGuardrailNames).filter( + (n) => !(values.guardrails || []).includes(n), + ); + const updateData: any = { team_id: teamId, team_alias: values.team_alias, @@ -504,9 +502,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)), + opted_out_global_guardrails: optedOutGlobalGuardrails, ...(values.logging_settings?.length > 0 ? { logging: values.logging_settings } : {}), - disable_global_guardrails: values.disable_global_guardrails || false, + disable_global_guardrails: killSwitchOnAtSave, soft_budget_alerting_emails: typeof values.soft_budget_alerting_emails === "string" ? values.soft_budget_alerting_emails @@ -610,6 +609,39 @@ const TeamInfoView: React.FC = ({ const { team_info: info } = teamData; + const initialKillSwitchOn = info.metadata?.disable_global_guardrails === true; + const optedOutGlobals = new Set(info.metadata?.opted_out_global_guardrails || []); + const nonGlobalOptIns: string[] = (info.metadata?.guardrails || []).filter( + (n: string) => !globalGuardrailNames.has(n), + ); + const effectiveGuardrails: string[] = initialKillSwitchOn + ? nonGlobalOptIns + : [ + ...Array.from(globalGuardrailNames).filter((n) => !optedOutGlobals.has(n)), + ...nonGlobalOptIns, + ]; + + const preventTagMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const renderGuardrailTag = ({ label, value, closable, onClose }: any) => { + const isGlobal = globalGuardrailNames.has(value); + return ( + + {isGlobal && } + {label} + + ); + }; + const copyToClipboard = async (text: string, key: string) => { const success = await utilCopyToClipboard(text); if (success) { @@ -738,23 +770,13 @@ const TeamInfoView: React.FC = ({ /> - Guardrails - {info.guardrails && info.guardrails.length > 0 ? ( -
- {info.guardrails.map((guardrail: string, index: number) => ( - - {guardrail} - - ))} -
- ) : ( - No guardrails configured - )} - {info.metadata?.disable_global_guardrails && ( -
- Global Guardrails Disabled -
- )} +
@@ -839,10 +861,23 @@ const TeamInfoView: React.FC = ({ )}
- {isEditing ? ( + {isEditing && isGuardrailsLoading ? ( +
Loading...
+ ) : isEditing ? (
{ + if ("disable_global_guardrails" in changedValues) { + const checked = changedValues.disable_global_guardrails === true; + const current = (form.getFieldValue("guardrails") || []) as string[]; + const nonGlobals = current.filter((n) => !globalGuardrailNames.has(n)); + form.setFieldValue( + "guardrails", + checked ? nonGlobals : [...Array.from(globalGuardrailNames), ...nonGlobals], + ); + } + }} initialValues={{ ...info, team_alias: info.team_alias, @@ -866,7 +901,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 +1125,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 +1214,6 @@ const TeamInfoView: React.FC = ({ } name="policies" - help="Select existing policies or enter new ones" >