From b2d831745dc7730bfc1d0117404cc1365545ab5d Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Wed, 26 Aug 2026 13:09:35 -0700 Subject: [PATCH] fix(ui): lock mode chips for tag-based guardrails in CustomCodeModal - chips still display the flattened modes but disable editing when the stored mode is tag-based, with a hint, so a chip edit can't silently discard tag routing - drop two redundant helper comments in guardrail_info_helpers --- .../custom_code/CustomCodeModal.test.tsx | 35 +++++++++++++++++++ .../custom_code/CustomCodeModal.tsx | 9 ++++- .../_components/guardrail_info_helpers.tsx | 2 -- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.test.tsx index 293261d4a1b..351d7bda101 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.test.tsx @@ -14,6 +14,8 @@ const mockCreate = vi.mocked(createGuardrailCall); const mockUpdate = vi.mocked(updateGuardrailCall); const mockTest = vi.mocked(testCustomCodeGuardrail); +const TAG_BASED_MODE_HINT = "Tag-based mode is managed in config and can't be edited here"; + describe("CustomCodeModal", () => { const onClose = vi.fn(); const onSuccess = vi.fn(); @@ -94,6 +96,39 @@ describe("CustomCodeModal", () => { expect(payload.litellm_params).not.toHaveProperty("mode"); }); + it("should lock the mode chips when editing a tag-based mode", async () => { + renderModal({ + editData: { + guardrail_id: "g-1", + guardrail_name: "tagged-guardrail", + litellm_params: { + mode: { tags: { "team:internal": "post_call" }, default: ["pre_call"] }, + default_on: false, + custom_code: "def apply_guardrail(): pass", + }, + }, + }); + + expect(await screen.findByLabelText("pre_call (Request)")).toBeInTheDocument(); + expect(screen.getByLabelText("post_call (Response)")).toBeInTheDocument(); + expect(screen.getAllByRole("combobox")[0]).toBeDisabled(); + expect(screen.getByText(TAG_BASED_MODE_HINT)).toBeInTheDocument(); + }); + + it("should leave the mode chips editable when editing a plain mode", async () => { + renderModal({ + editData: { + guardrail_id: "g-1", + guardrail_name: "plain-guardrail", + litellm_params: { mode: ["pre_call"], default_on: false, custom_code: "def apply_guardrail(): pass" }, + }, + }); + + expect(await screen.findByLabelText("pre_call (Request)")).toBeInTheDocument(); + expect(screen.getAllByRole("combobox")[0]).toBeEnabled(); + expect(screen.queryByText(TAG_BASED_MODE_HINT)).not.toBeInTheDocument(); + }); + it("should keep save disabled until a guardrail name is entered", async () => { const user = userEvent.setup(); renderModal(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.tsx index 849f586b3ee..d15ee06a7d8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.tsx @@ -31,7 +31,7 @@ import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import type { GuardrailLitellmParams, GuardrailMode } from "@/components/guardrails/types"; -import { guardrailModeList } from "../guardrail_info_helpers"; +import { guardrailModeList, isTagBasedMode } from "../guardrail_info_helpers"; // Code templates const CODE_TEMPLATES = { @@ -190,6 +190,7 @@ interface CustomCodeModalProps { const CustomCodeModal: React.FC = ({ visible, onClose, onSuccess, accessToken, editData }) => { const anchor = useComboboxAnchor(); const isEditMode = !!editData; + const tagBasedMode = isEditMode && isTagBasedMode(editData?.litellm_params?.mode); const [guardrailName, setGuardrailName] = useState(""); const [mode, setMode] = useState(["pre_call"]); const [defaultOn, setDefaultOn] = useState(false); @@ -519,6 +520,7 @@ const CustomCodeModal: React.FC = ({ visible, onClose, onS items={MODE_OPTIONS} value={selectedModeOptions} onValueChange={(options: ModeOption[]) => setMode(options.map((option) => option.value))} + disabled={tagBasedMode} multiple > } className="w-full"> @@ -540,6 +542,11 @@ const CustomCodeModal: React.FC = ({ visible, onClose, onS + {tagBasedMode && ( +

+ Tag-based mode is managed in config and can't be edited here +

+ )}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx index 11244212be6..0d50b3d32fa 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.tsx @@ -103,7 +103,6 @@ export const populateGuardrailProviderMap = (providerParamsResponse: Record { if (Array.isArray(raw)) return raw.filter((m): m is string => typeof m === "string"); if (typeof raw === "string") return [raw]; @@ -113,7 +112,6 @@ export const toModeArray = (raw: unknown): string[] => { export const isTagBasedMode = (raw: unknown): raw is { tags?: Record; default?: unknown } => raw !== null && typeof raw === "object" && !Array.isArray(raw); -// Every mode a guardrail can run in, with a tag-based mode's per-tag and default modes flattened and deduped export const guardrailModeList = (raw: unknown): string[] => { if (!isTagBasedMode(raw)) return toModeArray(raw); const tagged: string[] = raw.tags && typeof raw.tags === "object" ? Object.values(raw.tags).flatMap(toModeArray) : [];