diff --git a/litellm/proxy/guardrails/guardrail_registry.py b/litellm/proxy/guardrails/guardrail_registry.py index ea06f4993b1..404468a387d 100644 --- a/litellm/proxy/guardrails/guardrail_registry.py +++ b/litellm/proxy/guardrails/guardrail_registry.py @@ -472,7 +472,7 @@ def _configure_callback_scoping( raise ValueError(logging_only_scope_error) verbose_proxy_logger.error( "%s Ignoring logging_only_scope; the guardrail keeps its configured mode.", - logging_only_scope_error, + logging_only_scope_error.replace("\r", "").replace("\n", ""), ) custom_guardrail_callback.logging_only_scope = None else: 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 040d04b7f48..230be13ef33 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 { fireEvent, screen, waitFor } from "@testing-library/react"; +import { 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,34 +151,6 @@ 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 d99a31fedf0..0979a1e27a6 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 @@ -18,6 +18,7 @@ import { getSupportedModesForProvider, guardrail_provider_map, modeIncludesLoggingOnly, + normalizeLoggingOnlyScopeChoice, populateGuardrailProviderMap, populateGuardrailProviders, shouldRenderContentFilterConfigSettings, @@ -247,9 +248,8 @@ const AddGuardrailForm: React.FC = ({ visible, onClose, a useEffect(() => { const scopeChoice = form.getValues("logging_only_scope_choice"); - if (!directionalScopeSupported && (scopeChoice === "input" || scopeChoice === "output")) { - form.setValue("logging_only_scope_choice", "default"); - } + const normalizedScopeChoice = normalizeLoggingOnlyScopeChoice(scopeChoice, directionalScopeSupported); + if (normalizedScopeChoice !== scopeChoice) form.setValue("logging_only_scope_choice", normalizedScopeChoice); }, [directionalScopeSupported, form]); // Fetch guardrail UI settings + provider params on mount / accessToken change 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 57748d3fdb8..aa8515874b0 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,8 +38,8 @@ import { getLoggingOnlyScopeUpdate, getGuardrailLogoAndName, guardrail_provider_map, - choiceToLoggingOnlyScope, loggingOnlyScopeToChoice, + normalizeLoggingOnlyScopeChoice, skipSystemMessageToChoice, skipToolMessageToChoice, supportsDirectionalLoggingOnlyScope, @@ -225,25 +225,31 @@ 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", 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); + 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); } }, [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"); - } + const scopeChoice = form.getValues("logging_only_scope_choice"); + const normalizedScopeChoice = normalizeLoggingOnlyScopeChoice(scopeChoice, directionalScopeSupported); + if (normalizedScopeChoice !== scopeChoice) form.setValue("logging_only_scope_choice", normalizedScopeChoice); }, [directionalScopeSupported, form, guardrailData]); const resetToolPermissionEditor = useCallback(() => { @@ -519,6 +525,7 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose, const isConfigGuardrail = guardrailData.guardrail_definition_location === "config"; + /* eslint-disable max-lines -- keep edit-form scope normalization with its owning view */ return (
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.test.tsx index de1be697221..96ade4b8c1b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info_helpers.test.tsx @@ -21,6 +21,7 @@ import { getLoggingOnlyScopeOptions, formatLoggingOnlyScope, modeIncludesLoggingOnly, + normalizeLoggingOnlyScopeChoice, supportsDirectionalLoggingOnlyScope, } from "./guardrail_info_helpers"; @@ -248,6 +249,15 @@ describe("guardrail_info_helpers", () => { }); describe("logging-only scope helpers", () => { + it("normalizes directional choices only when the provider does not support them", () => { + expect(normalizeLoggingOnlyScopeChoice("input", false)).toBe("default"); + expect(normalizeLoggingOnlyScopeChoice("output", false)).toBe("default"); + expect(normalizeLoggingOnlyScopeChoice("both", false)).toBe("both"); + expect(normalizeLoggingOnlyScopeChoice("default", false)).toBe("default"); + expect(normalizeLoggingOnlyScopeChoice("input", true)).toBe("input"); + expect(normalizeLoggingOnlyScopeChoice("output", true)).toBe("output"); + }); + it("maps API scope values to choices and back", () => { expect(loggingOnlyScopeToChoice("input")).toBe("input"); expect(loggingOnlyScopeToChoice("output")).toBe("output"); 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 da28e0962a8..30162a2b942 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 @@ -118,6 +118,12 @@ export type LoggingOnlyScope = "input" | "output" | "both"; export type LoggingOnlyScopeChoice = "default" | LoggingOnlyScope; export type LoggingOnlyScopeOption = { label: string; value: LoggingOnlyScopeChoice }; +export const normalizeLoggingOnlyScopeChoice = ( + choice: LoggingOnlyScopeChoice, + directionalScopeSupported: boolean, +): LoggingOnlyScopeChoice => + directionalScopeSupported || choice === "default" || choice === "both" ? choice : "default"; + const LOGGING_ONLY_SCOPE_OPTIONS: LoggingOnlyScopeOption[] = [ { label: "Default (request and response)", value: "default" }, { label: "Input only (request)", value: "input" },