- {/* Header */}
-
-
Create guardrail
-
-
+
+
+ {/* Header */}
+
+
Create guardrail
+
+
- {/* Scrollable content - inline vertical stepper */}
-
- {/* Bottom bar */}
-
-
- {currentStep > 0 &&
}
- {currentStep < stepConfigs.length - 1 ? (
-
-
+
);
};
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.characterization.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.characterization.test.tsx
new file mode 100644
index 00000000000..d1b5c49bacd
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_info.characterization.test.tsx
@@ -0,0 +1,289 @@
+import * as networking from "@/components/networking";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import GuardrailInfoView from "./guardrail_info";
+
+vi.mock("@/components/networking", () => ({
+ getGuardrailInfo: vi.fn(),
+ getGuardrailUISettings: vi.fn(),
+ getGuardrailProviderSpecificParams: vi.fn(),
+ updateGuardrailCall: vi.fn(),
+}));
+
+vi.mock("./content_filter/ContentFilterManager", () => ({
+ __esModule: true,
+ default: ({ isEditing }: { isEditing: boolean }) => (
+
{isEditing && Stray Action}
+ ),
+ formatContentFilterDataForAPI: () => ({ patterns: [], blocked_words: [], categories: [] }),
+}));
+
+const uiSettings = {
+ supported_entities: [],
+ supported_actions: [],
+ pii_entity_categories: [],
+ supported_modes: ["pre_call", "post_call"],
+};
+
+const bedrockParams = {
+ bedrock: {
+ guardrailIdentifier: { description: "The guardrail id on Bedrock", required: true, type: null },
+ api_key: { description: "API key", required: false, type: null },
+ optional_params: {
+ description: "Optional parameters",
+ required: false,
+ type: "nested",
+ fields: {
+ severity_threshold: { description: "Severity threshold", required: false, type: "number" },
+ },
+ },
+ },
+};
+
+const numericProviderParams = {
+ bedrock: {
+ guardrailIdentifier: { description: "The guardrail id on Bedrock", required: true, type: null },
+ max_tokens: { description: "Token ceiling", required: false, type: "number" },
+ },
+};
+
+const nestedProviderParams = {
+ bedrock: {
+ guardrailIdentifier: { description: "The guardrail id on Bedrock", required: true, type: null },
+ tuning: {
+ description: "Nested tuning block",
+ required: false,
+ type: "nested",
+ fields: {
+ retries: { description: "How many retries", required: false, type: null },
+ },
+ },
+ },
+};
+
+const guardrail = (litellmParams: Record
, guardrailInfo?: Record) => ({
+ guardrail_id: "123",
+ guardrail_name: "Test Guardrail",
+ litellm_params: { guardrail: "bedrock", mode: "pre_call", default_on: true, ...litellmParams },
+ guardrail_info: guardrailInfo,
+ created_at: "2024-01-01T00:00:00Z",
+ updated_at: "2024-01-01T00:00:00Z",
+ guardrail_definition_location: "database",
+});
+
+const openEditor = async (user: ReturnType) => {
+ await user.click(await screen.findByText("Settings"));
+ await user.click(await screen.findByText("Edit Settings"));
+ await screen.findByLabelText("Guardrail Name");
+};
+
+const saveChanges = async (user: ReturnType) => {
+ await user.click(screen.getByText("Save Changes"));
+};
+
+const renderView = () => render( {}} accessToken="123" isAdmin />);
+
+const lastPayload = () => vi.mocked(networking.updateGuardrailCall).mock.calls.at(-1)?.[2];
+
+describe("GuardrailInfoView update payload characterization", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(networking.getGuardrailUISettings).mockResolvedValue(uiSettings);
+ vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue(bedrockParams);
+ vi.mocked(networking.updateGuardrailCall).mockResolvedValue({ status: "success" });
+ vi.mocked(networking.getGuardrailInfo).mockResolvedValue(
+ guardrail({ guardrailIdentifier: "gr-abc", api_key: "sk-old" }),
+ );
+ });
+
+ it("sends nothing at all when the user saves without touching a field", async () => {
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+ await saveChanges(user);
+
+ await screen.findByText("Edit Settings");
+ expect(networking.updateGuardrailCall).not.toHaveBeenCalled();
+ });
+
+ it("sends only guardrail_name and drops the empty litellm_params object", async () => {
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ const nameInput = screen.getByLabelText("Guardrail Name");
+ await user.clear(nameInput);
+ await user.type(nameInput, "Renamed Guardrail");
+ await saveChanges(user);
+
+ await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
+ expect(lastPayload()).toEqual({ guardrail_name: "Renamed Guardrail" });
+ });
+
+ it("never leaks the seeded guardrail, mode or created_at keys into litellm_params", async () => {
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ const identifier = screen.getByLabelText("guardrailIdentifier");
+ await user.clear(identifier);
+ await user.type(identifier, "gr-new");
+ await saveChanges(user);
+
+ await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
+ expect(lastPayload()).toEqual({ litellm_params: { guardrailIdentifier: "gr-new" } });
+ });
+
+ it("sends null for a provider param the user cleared", async () => {
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ await user.clear(screen.getByLabelText("api_key"));
+ await saveChanges(user);
+
+ await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
+ expect(lastPayload()).toEqual({ litellm_params: { api_key: null } });
+ });
+
+ it("maps the skip system message choice to an explicit boolean", async () => {
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ await user.click(screen.getByLabelText("Skip system messages in guardrail"));
+ await user.click(await screen.findByText("Yes — exclude from guardrail scan"));
+ await saveChanges(user);
+
+ await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
+ expect(lastPayload()).toEqual({ litellm_params: { skip_system_message_in_guardrail: true } });
+ });
+
+ it("parses the guardrail information textarea into an object", async () => {
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ const infoBox = screen.getByLabelText("Guardrail Information");
+ await user.clear(infoBox);
+ await user.type(infoBox, '{{"team":"platform"}');
+ await saveChanges(user);
+
+ await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
+ expect(lastPayload()).toEqual({ guardrail_info: { team: "platform" } });
+ });
+
+ it("reads a value typed into the optional params section out of the nested optional_params object", async () => {
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ const threshold = screen.getByPlaceholderText("Severity threshold");
+ await user.type(threshold, "4");
+ await saveChanges(user);
+
+ await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
+ expect(lastPayload()).toEqual({ litellm_params: { severity_threshold: 4 } });
+ });
+
+ it("keeps a dotted nested provider field out of the payload entirely", async () => {
+ vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue(nestedProviderParams);
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ await user.type(screen.getByLabelText("retries"), "7");
+ const identifier = screen.getByLabelText("guardrailIdentifier");
+ await user.clear(identifier);
+ await user.type(identifier, "gr-new");
+ await saveChanges(user);
+
+ await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
+ expect(lastPayload()).toEqual({ litellm_params: { guardrailIdentifier: "gr-new" } });
+ });
+
+ it("clears a stored nested param to null because no form field ever binds it", async () => {
+ vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue(nestedProviderParams);
+ vi.mocked(networking.getGuardrailInfo).mockResolvedValue(
+ guardrail({ guardrailIdentifier: "gr-abc", tuning: { retries: 2 } }),
+ );
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ const identifier = screen.getByLabelText("guardrailIdentifier");
+ await user.clear(identifier);
+ await user.type(identifier, "gr-new");
+ await saveChanges(user);
+
+ await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
+ expect(lastPayload()).toEqual({ litellm_params: { guardrailIdentifier: "gr-new", tuning: null } });
+ });
+
+ it("sends an unnormalised numeric provider field as the raw string the input produced", async () => {
+ vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue(numericProviderParams);
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ await user.type(screen.getByPlaceholderText("Token ceiling"), "5");
+ await saveChanges(user);
+
+ await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
+ expect(lastPayload()).toEqual({ litellm_params: { max_tokens: "5" } });
+ });
+
+ it("blocks the save when the required guardrail name is cleared", async () => {
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ await user.clear(screen.getByLabelText("Guardrail Name"));
+ await saveChanges(user);
+
+ await screen.findByText("Please input a guardrail name");
+ expect(networking.updateGuardrailCall).not.toHaveBeenCalled();
+ });
+
+ it("saves when the user presses Enter in a text field", async () => {
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ const nameInput = screen.getByLabelText("Guardrail Name");
+ await user.clear(nameInput);
+ await user.type(nameInput, "Renamed Guardrail{Enter}");
+
+ await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
+ expect(lastPayload()).toEqual({ guardrail_name: "Renamed Guardrail" });
+ });
+
+ it("also saves when any other button inside the form is clicked", async () => {
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ const nameInput = screen.getByLabelText("Guardrail Name");
+ await user.clear(nameInput);
+ await user.type(nameInput, "Renamed Guardrail");
+ await user.click(screen.getByText("Stray Action"));
+
+ await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
+ expect(lastPayload()).toEqual({ guardrail_name: "Renamed Guardrail" });
+ });
+
+ it("keeps edits made before Cancel when the editor is reopened", async () => {
+ const user = userEvent.setup({ delay: null });
+ renderView();
+ await openEditor(user);
+
+ const nameInput = screen.getByLabelText("Guardrail Name");
+ await user.clear(nameInput);
+ await user.type(nameInput, "Draft Name");
+ await user.click(screen.getByText("Cancel"));
+
+ await user.click(await screen.findByText("Edit Settings"));
+ expect(await screen.findByLabelText("Guardrail Name")).toHaveValue("Draft Name");
+ });
+});
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 0e9f6b97b3e..f8d7bede021 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
@@ -8,11 +8,28 @@ import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { CodeOutlined, EyeInvisibleOutlined, InfoCircleOutlined, StopOutlined } from "@ant-design/icons";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import { Badge, Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react";
-import { Button, Divider, Form, Input, Select, Tooltip } from "antd";
+import { Button as AntdButton, Tooltip } from "antd";
import { CheckIcon, CopyIcon } from "lucide-react";
-import React, { useCallback, useEffect, useState } from "react";
+import React, { useCallback, useEffect, useLayoutEffect, useState } from "react";
+import { useForm } from "react-hook-form";
import { toast } from "@/lib/toast";
import { Logo } from "@/components/molecules/logo/Logo";
+import { FieldGroup } from "@/components/shared/form/field";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Separator } from "@/components/ui/separator";
+import { Textarea } from "@/components/ui/textarea";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import {
+ asText,
+ GuardrailField,
+ labelWithHint,
+ readRecord,
+ requiredRule,
+ type GuardrailFormValues,
+ SkipMessageSelect,
+} from "./GuardrailFormField";
import ContentFilterManager, { formatContentFilterDataForAPI } from "./content_filter/ContentFilterManager";
import CustomCodeModal, { EditGuardrailData } from "./custom_code/CustomCodeModal";
import {
@@ -28,6 +45,18 @@ import GuardrailProviderFields from "./guardrail_provider_fields";
import PiiConfiguration from "./pii_configuration";
import ToolPermissionRulesEditor, { ToolPermissionConfig } from "./tool_permission/ToolPermissionRulesEditor";
+const DEFAULT_ON_ITEMS = [
+ { label: "Yes", value: true },
+ { label: "No", value: false },
+];
+
+const SectionHeading: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+
+ {children}
+
+
+);
+
export interface GuardrailInfoProps {
guardrailId: string;
onClose: () => void;
@@ -40,7 +69,7 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose,
const [guardrailProviderSpecificParams, setGuardrailProviderSpecificParams] = useState(null);
const [loading, setLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
- const [form] = Form.useForm();
+ const form = useForm({ defaultValues: {} });
const [selectedPiiEntities, setSelectedPiiEntities] = useState([]);
const [selectedPiiActions, setSelectedPiiActions] = useState<{ [key: string]: string }>({});
const [guardrailSettings, setGuardrailSettings] = useState<{
@@ -183,25 +212,26 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose,
fetchGuardrailUISettings();
}, [guardrailId, accessToken]);
- // Reset form when guardrail data or provider params change
+ // Reset form when guardrail data or provider params change. Only the names this form actually
+ // binds are seeded: an unbound key would otherwise be submitted as if the user had set it.
useEffect(() => {
- if (guardrailData && form) {
- const lp = { ...(guardrailData.litellm_params || {}) };
- delete lp.skip_system_message_in_guardrail;
- delete lp.skip_tool_message_in_guardrail;
- form.setFieldsValue({
- guardrail_name: guardrailData.guardrail_name,
- ...lp,
- skip_system_message_choice: skipSystemMessageToChoice(
- guardrailData.litellm_params?.skip_system_message_in_guardrail,
- ),
- skip_tool_message_choice: skipToolMessageToChoice(guardrailData.litellm_params?.skip_tool_message_in_guardrail),
- guardrail_info: guardrailData.guardrail_info ? JSON.stringify(guardrailData.guardrail_info, null, 2) : "",
- // Include any optional_params if they exist
- ...(guardrailData.litellm_params?.optional_params && {
- optional_params: guardrailData.litellm_params.optional_params,
- }),
- });
+ if (!guardrailData) return;
+ form.setValue("guardrail_name", guardrailData.guardrail_name);
+ form.setValue("default_on", guardrailData.litellm_params?.default_on);
+ 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]);
@@ -245,7 +275,7 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose,
}));
};
- const handleGuardrailUpdate = async (values: any) => {
+ const handleGuardrailUpdate = async (values: GuardrailFormValues) => {
try {
if (!accessToken) return;
@@ -290,7 +320,7 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose,
// Only include guardrail_info if it has changed
const originalGuardrailInfo = guardrailData.guardrail_info;
- const newGuardrailInfo = values.guardrail_info ? JSON.parse(values.guardrail_info) : undefined;
+ const newGuardrailInfo = values.guardrail_info ? JSON.parse(asText(values.guardrail_info)) : undefined;
if (JSON.stringify(originalGuardrailInfo) !== JSON.stringify(newGuardrailInfo)) {
updateData.guardrail_info = newGuardrailInfo;
}
@@ -390,10 +420,11 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose,
return;
}
// Check for both direct parameter name and nested optional_params object
- let paramValue = values[paramName];
- if (paramValue === undefined || paramValue === null || paramValue === "") {
- paramValue = values.optional_params?.[paramName];
- }
+ const directValue = values[paramName];
+ const paramValue =
+ directValue === undefined || directValue === null || directValue === ""
+ ? readRecord(values.optional_params, paramName)
+ : directValue;
// Get the original value for comparison
const originalValue = guardrailData.litellm_params?.[paramName];
@@ -437,6 +468,14 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose,
}
};
+ // antd re-read onFinish at validation-resolution time, so a submit fired by the same click that
+ // updated state saw that state; a captured handler would not.
+ const submitRef = React.useRef(handleGuardrailUpdate);
+ useLayoutEffect(() => {
+ submitRef.current = handleGuardrailUpdate;
+ });
+ const submitLatest = useCallback((values: GuardrailFormValues) => submitRef.current(values), []);
+
if (loading) {
return Loading...
;
}
@@ -470,22 +509,22 @@ const GuardrailInfoView: React.FC = ({ guardrailId, onClose,
return (
-
} onClick={onClose} className="mb-4">
+
} onClick={onClose} className="mb-4">
Back to Guardrails
-
+
{guardrailData.guardrail_name || "Unnamed Guardrail"}
- {guardrailData.guardrail_id}
+ {guardrailData.guardrail_id}
- : }
onClick={() => copyToClipboard(guardrailData.guardrail_id, "guardrail-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["guardrail-id"]
- ? "text-green-600 bg-green-50 border-green-200"
- : "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
+ ? "text-green-600 bg-green-50 border-green-200 dark:text-green-400 dark:bg-green-950/40 dark:border-green-900"
+ : "text-muted-foreground hover:text-foreground hover:bg-muted"
}`}
/>
@@ -545,14 +584,14 @@ const GuardrailInfoView: React.FC
= ({ guardrailId, onClose,
PII Entity Configuration
-
-
Entity Type
-
Configuration
+
+ Entity Type
+ Configuration
{Object.entries(guardrailData.litellm_params?.pii_entities_config).map(([key, value]) => (
-
-
{key}
+
+ {key}
= ({ guardrailId, onClose,
Custom Code
{isAdmin && !isConfigGuardrail && (
-
} onClick={() => setCustomCodeModalVisible(true)}>
+
} onClick={() => setCustomCodeModalVisible(true)}>
Edit Code
-
+
)}
@@ -624,172 +663,171 @@ const GuardrailInfoView: React.FC
= ({ guardrailId, onClose,
{!isEditing &&
!isConfigGuardrail &&
(guardrailData.litellm_params?.guardrail === "custom_code" ? (
- } onClick={() => setCustomCodeModalVisible(true)}>
+ } onClick={() => setCustomCodeModalVisible(true)}>
Edit Code
-
+
) : (
- setIsEditing(true)}>Edit Settings
+ setIsEditing(true)}>Edit Settings
))}
{isEditing ? (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {guardrailData.litellm_params?.guardrail === "presidio" && (
- <>
-
PII Protection
-
- >
- )}
+
-
+
+ {({ id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": describedBy }) => (
+
+ )}
+
- {(guardrailData.litellm_params?.guardrail === "tool_permission" ||
- guardrailProviderSpecificParams) &&
Provider Settings}
+
+ {(fieldControl) => }
+
- {guardrailData.litellm_params?.guardrail === "tool_permission" ? (
-
- ) : (
- <>
- {/* Provider-specific fields */}
-
guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail,
- ) || null
- }
+
+ {(fieldControl) => }
+
+ {guardrailData.litellm_params?.guardrail === "presidio" && (
+ <>
+ PII Protection
+
+ {guardrailSettings && (
+
+ )}
+
+ >
+ )}
+
+
- {/* Optional parameters */}
- {guardrailProviderSpecificParams &&
- (() => {
- const currentProvider = Object.keys(guardrail_provider_map).find(
- (key) => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail,
- );
- if (!currentProvider) return null;
+ {(guardrailData.litellm_params?.guardrail === "tool_permission" ||
+ guardrailProviderSpecificParams) && Provider Settings}
- const providerKey = guardrail_provider_map[currentProvider]?.toLowerCase();
- const providerFields = guardrailProviderSpecificParams[providerKey];
+ {guardrailData.litellm_params?.guardrail === "tool_permission" ? (
+
+ ) : (
+ <>
+ {/* Provider-specific fields */}
+ guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail,
+ ) || null
+ }
+ control={form.control}
+ accessToken={accessToken}
+ providerParams={guardrailProviderSpecificParams}
+ value={guardrailData.litellm_params}
+ />
- if (!providerFields || !providerFields.optional_params) return null;
+ {/* Optional parameters */}
+ {guardrailProviderSpecificParams &&
+ (() => {
+ const currentProvider = Object.keys(guardrail_provider_map).find(
+ (key) => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail,
+ );
+ if (!currentProvider) return null;
- return (
-
- );
- })()}
- >
- )}
+ const providerKey = guardrail_provider_map[currentProvider]?.toLowerCase();
+ const providerFields = guardrailProviderSpecificParams[providerKey];
- Advanced Settings
-
-
-
+ if (!providerFields || !providerFields.optional_params) return null;
-
- {
- setIsEditing(false);
- setHasUnsavedContentFilterChanges(false);
- resetToolPermissionEditor();
- }}
- >
- Cancel
-
-
- Save Changes
-
-
-
+ return (
+
+ );
+ })()}
+ >
+ )}
+
+ Advanced Settings
+
+ {({ ref, value, ...field }) => (
+
+ )}
+
+
+
+ {
+ setIsEditing(false);
+ setHasUnsavedContentFilterChanges(false);
+ resetToolPermissionEditor();
+ }}
+ >
+ Cancel
+
+ Save Changes
+
+
+
+
) : (
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_optional_params.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_optional_params.tsx
index 8b119c4933c..2dcc0c69ed1 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_optional_params.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_optional_params.tsx
@@ -1,8 +1,19 @@
import React from "react";
-import { Form, Select, Typography, Input, Button } from "antd";
+import { MultiSelect } from "@/components/shared/MultiSelect";
+import { PasswordInput } from "@/components/shared/PasswordInput";
import NumericalInput from "@/components/shared/numerical_input";
-
-const { Title } = Typography;
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import {
+ asStringArray,
+ asText,
+ GuardrailField,
+ readRecord,
+ requiredRule,
+ type GuardrailFieldControlProps,
+ type GuardrailFormControl,
+} from "./GuardrailFormField";
interface ProviderParam {
param: string;
@@ -19,17 +30,55 @@ interface ProviderParam {
interface GuardrailOptionalParamsProps {
optionalParams: ProviderParam;
parentFieldKey: string;
- values?: Record
;
+ control: GuardrailFormControl;
+ values?: Record;
}
interface DictFieldProps {
field: ProviderParam;
- fieldKey: string;
- fullFieldKey: string | string[];
- value: any | null;
+ fullFieldKey: string;
+ control: GuardrailFormControl;
+ value: unknown;
}
-const DictField: React.FC = ({ field, fieldKey, fullFieldKey, value }) => {
+const BOOLEAN_ITEMS = [
+ { label: "True", value: true },
+ { label: "False", value: false },
+];
+
+const isSecretKey = (fieldKey: string): boolean =>
+ fieldKey.includes("password") || fieldKey.includes("secret") || fieldKey.includes("key");
+
+const toNumberValue = (raw: unknown): unknown => {
+ if (raw === null || raw === undefined || raw === "") return undefined;
+ const num = Number(raw);
+ return isNaN(num) ? raw : num;
+};
+
+const BooleanSelect: React.FC<{ control: GuardrailFieldControlProps; placeholder: string }> = ({
+ control,
+ placeholder,
+}) => {
+ const { id, value, onChange, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy } = control;
+
+ return (
+
+ );
+};
+
+const DictField: React.FC = ({ field, fullFieldKey, control, value }) => {
const [selectedEntries, setSelectedEntries] = React.useState>([]);
const [availableKeys, setAvailableKeys] = React.useState(field.dict_key_options || []);
@@ -69,36 +118,58 @@ const DictField: React.FC = ({ field, fieldKey, fullFieldKey, va
{/* Existing entries */}
{selectedEntries.map((entry) => (
-
-
{entry.key}
-
-
{
- if (value === null || value === undefined || value === "") return undefined;
- const num = Number(value);
- return isNaN(num) ? value : num;
+
+
+ {(fieldControl) => {
+ if (field.dict_value_type === "number") {
+ return (
+ ) =>
+ fieldControl.onChange(toNumberValue(event.target.value))
}
- : undefined
+ onBlur={fieldControl.onBlur}
+ aria-invalid={fieldControl["aria-invalid"]}
+ aria-describedby={fieldControl["aria-describedby"]}
+ />
+ );
}
- >
- {field.dict_value_type === "number" ? (
-