fix(guardrails): normalize logging-only scope and sanitize warning logs

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-29 10:54:25 +00:00
parent bc423af734
commit d3bb9a6a4a
6 changed files with 43 additions and 48 deletions

View file

@ -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:

View file

@ -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();

View file

@ -18,6 +18,7 @@ import {
getSupportedModesForProvider,
guardrail_provider_map,
modeIncludesLoggingOnly,
normalizeLoggingOnlyScopeChoice,
populateGuardrailProviderMap,
populateGuardrailProviders,
shouldRenderContentFilterConfigSettings,
@ -247,9 +248,8 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ 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

View file

@ -38,8 +38,8 @@ import {
getLoggingOnlyScopeUpdate,
getGuardrailLogoAndName,
guardrail_provider_map,
choiceToLoggingOnlyScope,
loggingOnlyScopeToChoice,
normalizeLoggingOnlyScopeChoice,
skipSystemMessageToChoice,
skipToolMessageToChoice,
supportsDirectionalLoggingOnlyScope,
@ -225,25 +225,31 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ 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<GuardrailInfoProps> = ({ guardrailId, onClose,
const isConfigGuardrail = guardrailData.guardrail_definition_location === "config";
/* eslint-disable max-lines -- keep edit-form scope normalization with its owning view */
return (
<div className="p-4">
<div>

View file

@ -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");

View file

@ -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" },