diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.characterization.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.characterization.test.tsx index 230be13ef33..040d04b7f48 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.characterization.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.characterization.test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { screen, waitFor } from "@testing-library/react"; +import { fireEvent, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { chooseSelectOption, renderWithProviders } from "@/../tests/test-utils"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -151,6 +151,34 @@ describe("AddGuardrailForm create payload characterization", () => { expect(screen.getByRole("option", { name: "Both (request and response)" })).toBeInTheDocument(); }); + it("clears a selected directional scope when switching to an unsupported provider", async () => { + vi.mocked(networking.getGuardrailUISettings).mockResolvedValue({ + ...uiSettings, + supported_modes: ["pre_call", "logging_only"], + providers_without_directional_logging_only_scope: ["xecguard"], + }); + vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({ + ...providerParams, + xecguard: { ui_friendly_name: "XecGuard" }, + }); + const user = userEvent.setup({ delay: null }); + renderForm(); + + fireEvent.change(await screen.findByLabelText("Guardrail Name"), { target: { value: "switch-scope" } }); + await pickProvider(user, "Bedrock Guardrail"); + await user.click(screen.getByLabelText("Mode")); + await user.click((await screen.findAllByText("logging_only")).at(-1) as HTMLElement); + await chooseSelectOption(user, await screen.findByLabelText("Logging only scope"), "Output only (response)"); + expect(screen.getByLabelText("Logging only scope")).toHaveTextContent("Output only (response)"); + await pickProvider(user, "XecGuard"); + await user.click(screen.getByRole("button", { name: "Next" })); + await user.click(await screen.findByRole("button", { name: "Create Guardrail" })); + + await waitFor(() => expect(networking.createGuardrailCall).toHaveBeenCalledTimes(1)); + expect(payload()?.litellm_params.mode).toContain("logging_only"); + expect(payload()?.litellm_params).not.toHaveProperty("logging_only_scope"); + }); + it("hides logging-only scope and omits it from a pre-call payload", async () => { const user = userEvent.setup({ delay: null }); renderForm(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx index 4c3f9ce693e..d99a31fedf0 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/add_guardrail_form.tsx @@ -243,6 +243,14 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a const providerValue = guardrail_provider_map[selectedProvider]; return (providerValue || "").toLowerCase() === "tool_permission"; }, [selectedProvider]); + const directionalScopeSupported = supportsDirectionalLoggingOnlyScope(guardrailSettings, selectedProvider); + + useEffect(() => { + const scopeChoice = form.getValues("logging_only_scope_choice"); + if (!directionalScopeSupported && (scopeChoice === "input" || scopeChoice === "output")) { + form.setValue("logging_only_scope_choice", "default"); + } + }, [directionalScopeSupported, form]); // Fetch guardrail UI settings + provider params on mount / accessToken change useEffect(() => { @@ -690,7 +698,6 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a const providerLabels: Record = getGuardrailProviders(); const providerKeys = Object.keys(providerLabels); const supportedModes = getSupportedModesForProvider(guardrailSettings, selectedProvider) ?? DEFAULT_MODES; - const directionalScopeSupported = supportsDirectionalLoggingOnlyScope(guardrailSettings, selectedProvider); return ( { expect(lastPayload()).toEqual({ litellm_params: { logging_only_scope: null } }); }); + it("clears a stored directional scope for a provider that does not support it", async () => { + vi.mocked(networking.getGuardrailUISettings).mockResolvedValue({ + ...uiSettings, + providers_without_directional_logging_only_scope: ["bedrock"], + }); + vi.mocked(networking.getGuardrailInfo).mockResolvedValue( + guardrail({ guardrailIdentifier: "gr-abc", mode: "logging_only", logging_only_scope: "output" }), + ); + const user = userEvent.setup({ delay: null }); + renderView(); + await openEditor(user); + await saveChanges(user); + + await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1)); + expect(lastPayload()).toEqual({ litellm_params: { logging_only_scope: null } }); + }); + it("parses the guardrail information textarea into an object", async () => { const user = userEvent.setup({ delay: null }); renderView(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx index 73d9fb8123e..57748d3fdb8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.tsx @@ -38,6 +38,7 @@ import { getLoggingOnlyScopeUpdate, getGuardrailLogoAndName, guardrail_provider_map, + choiceToLoggingOnlyScope, loggingOnlyScopeToChoice, skipSystemMessageToChoice, skipToolMessageToChoice, @@ -224,27 +225,27 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, // binds are seeded: an unbound key would otherwise be submitted as if the user had set it. useEffect(() => { if (!guardrailData) return; + const litellmParams = guardrailData.litellm_params; form.setValue("guardrail_name", guardrailData.guardrail_name); - form.setValue("default_on", guardrailData.litellm_params?.default_on); - const storedLoggingOnlyScope = guardrailData.litellm_params?.logging_only_scope; - form.setValue("logging_only_scope_choice", loggingOnlyScopeToChoice(storedLoggingOnlyScope)); - form.setValue( - "skip_system_message_choice", - skipSystemMessageToChoice(guardrailData.litellm_params?.skip_system_message_in_guardrail), - ); - form.setValue( - "skip_tool_message_choice", - skipToolMessageToChoice(guardrailData.litellm_params?.skip_tool_message_in_guardrail), - ); - form.setValue( - "guardrail_info", - guardrailData.guardrail_info ? JSON.stringify(guardrailData.guardrail_info, null, 2) : "", - ); - if (guardrailData.litellm_params?.optional_params) { - form.setValue("optional_params", guardrailData.litellm_params.optional_params); + form.setValue("default_on", litellmParams?.default_on); + form.setValue("logging_only_scope_choice", loggingOnlyScopeToChoice(litellmParams?.logging_only_scope)); + const skipSystemMessageChoice = skipSystemMessageToChoice(litellmParams?.skip_system_message_in_guardrail); + form.setValue("skip_system_message_choice", skipSystemMessageChoice); + form.setValue("skip_tool_message_choice", skipToolMessageToChoice(litellmParams?.skip_tool_message_in_guardrail)); + const guardrailInfo = guardrailData.guardrail_info ? JSON.stringify(guardrailData.guardrail_info, null, 2) : ""; + form.setValue("guardrail_info", guardrailInfo); + if (litellmParams?.optional_params) { + form.setValue("optional_params", litellmParams.optional_params); } }, [guardrailData, guardrailProviderSpecificParams, form]); + useEffect(() => { + const scope = choiceToLoggingOnlyScope(form.getValues("logging_only_scope_choice")); + if (!directionalScopeSupported && scope !== null && scope !== "both") { + form.setValue("logging_only_scope_choice", "default"); + } + }, [directionalScopeSupported, form, guardrailData]); + const resetToolPermissionEditor = useCallback(() => { if (guardrailData?.litellm_params?.guardrail === "tool_permission") { setToolPermissionConfig({