+ Preview:{" "}
+ {
+ availableCategories.find((c) => c.name === selectedCategoryName)
+ ?.display_name
+ }
{categoryFileTypes[selectedCategoryName] && (
-
+
({categoryFileTypes[selectedCategoryName]?.toUpperCase()})
)}
+
Loading content...
) : previewYaml ? (
-
+
{previewYaml}
) : (
-
+
Unable to load category content
)}
@@ -337,83 +289,147 @@ const ContentCategoryConfiguration: React.FC
{selectedCategories.length > 0 ? (
<>
-
-
-
{
- const keyArray = Array.isArray(keys) ? keys : keys ? [keys] : [];
- const oldExpanded = new Set(expandedYamlCategories);
-
- // Find newly expanded categories and fetch their YAML
- keyArray.forEach((key) => {
- const categoryName = key as string;
- if (!oldExpanded.has(categoryName) && !categoryYaml[categoryName]) {
- fetchCategoryYaml(categoryName);
- }
- });
-
- setExpandedYamlCategories(keyArray as string[]);
- }}
- ghost
- items={selectedCategories.map((category) => {
- const fileType = categoryFileTypes[category.category] || 'yaml';
- const fileTypeLabel = fileType.toUpperCase();
-
- return {
- key: category.category,
- label: (
-
-
- View {fileTypeLabel} for {category.display_name}
-
- ),
- children: loadingYaml[category.category] ? (
-
- Loading content...
-
- ) : categoryYaml[category.category] ? (
-
- {categoryYaml[category.category]}
-
- ) : (
-
- Content will load when expanded
-
- ),
- };
+
+
+
+ Category
+ Action
+ Severity Threshold
+
+
+
+
+ {selectedCategories.map((record) => {
+ const category = availableCategories.find(
+ (c) => c.name === record.category,
+ );
+ return (
+
+
+
+
{record.display_name}
+ {category?.description && (
+
+ {category.description}
+
+ )}
+
+
+
+
+ onCategoryUpdate(record.id, "action", value)
+ }
+ >
+
+
+
+
+
+ BLOCK
+
+
+
+ MASK
+
+
+
+
+
+
+
+ onCategoryUpdate(
+ record.id,
+ "severity_threshold",
+ value,
+ )
+ }
+ >
+
+
+
+
+ Low
+ Medium
+ High
+
+
+
+
+ onCategoryRemove(record.id)}
+ >
+
+ Remove
+
+
+
+ );
})}
- />
-
+
+
+
+ {
+ const oldExpanded = new Set(expandedYamlCategories);
+ values.forEach((key) => {
+ if (!oldExpanded.has(key) && !categoryYaml[key]) {
+ fetchCategoryYaml(key);
+ }
+ });
+ setExpandedYamlCategories(values);
+ }}
+ >
+ {selectedCategories.map((category) => {
+ const fileType = categoryFileTypes[category.category] || "yaml";
+ const fileTypeLabel = fileType.toUpperCase();
+ return (
+
+
+
+
+
+ View {fileTypeLabel} for {category.display_name}
+
+
+
+
+ {loadingYaml[category.category] ? (
+
+ Loading content...
+
+ ) : categoryYaml[category.category] ? (
+
+ {categoryYaml[category.category]}
+
+ ) : (
+
+ Content will load when expanded
+
+ )}
+
+
+ );
+ })}
+
>
) : (
-
- No blocked topics selected. Add topics to detect and block harmful content.
+
+ No blocked topics selected. Add topics to detect and block harmful
+ content.
)}
@@ -421,4 +437,3 @@ const ContentCategoryConfiguration: React.FC
};
export default ContentCategoryConfiguration;
-
diff --git a/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentFilterConfiguration.tsx b/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentFilterConfiguration.tsx
index 7851f57c998..40249a2fc01 100644
--- a/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentFilterConfiguration.tsx
+++ b/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentFilterConfiguration.tsx
@@ -1,5 +1,6 @@
-import React, { useState } from "react";
-import { Typography, Space, Upload, Card, Button } from "antd";
+import React, { useRef, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
import { Plus as PlusOutlined, Upload as UploadOutlined } from "lucide-react";
import { validateBlockedWordsFile } from "../../networking";
import NotificationsManager from "../../molecules/notifications_manager";
@@ -13,8 +14,6 @@ import CompetitorIntentConfiguration, {
CompetitorIntentConfig,
} from "./CompetitorIntentConfiguration";
-const { Title, Text } = Typography;
-
interface PrebuiltPattern {
name: string;
display_name: string;
@@ -78,7 +77,7 @@ interface ContentFilterConfigurationProps {
competitorIntentConfig?: CompetitorIntentConfig | null;
onCompetitorIntentChange?: (
enabled: boolean,
- config: CompetitorIntentConfig | null
+ config: CompetitorIntentConfig | null,
) => void;
}
@@ -109,17 +108,24 @@ const ContentFilterConfiguration: React.FC = ({
}) => {
const [patternModalVisible, setPatternModalVisible] = useState(false);
const [keywordModalVisible, setKeywordModalVisible] = useState(false);
- const [customPatternModalVisible, setCustomPatternModalVisible] = useState(false);
+ const [customPatternModalVisible, setCustomPatternModalVisible] =
+ useState(false);
const [selectedPatternName, setSelectedPatternName] = useState("");
const [patternAction, setPatternAction] = useState<"BLOCK" | "MASK">("BLOCK");
const [customPatternName, setCustomPatternName] = useState("");
const [customPatternRegex, setCustomPatternRegex] = useState("");
- const [customPatternAction, setCustomPatternAction] = useState<"BLOCK" | "MASK">("BLOCK");
+ const [customPatternAction, setCustomPatternAction] = useState<
+ "BLOCK" | "MASK"
+ >("BLOCK");
const [newKeyword, setNewKeyword] = useState("");
- const [newKeywordAction, setNewKeywordAction] = useState<"BLOCK" | "MASK">("BLOCK");
- const [newKeywordDescription, setNewKeywordDescription] = useState("");
+ const [newKeywordAction, setNewKeywordAction] = useState<"BLOCK" | "MASK">(
+ "BLOCK",
+ );
+ const [newKeywordDescription, setNewKeywordDescription] =
+ useState("");
const [uploadValidating, setUploadValidating] = useState(false);
+ const fileInputRef = useRef(null);
const handleAddPrebuiltPattern = () => {
if (!selectedPatternName) {
@@ -127,7 +133,9 @@ const ContentFilterConfiguration: React.FC = ({
return;
}
- const selectedPattern = prebuiltPatterns.find((p) => p.name === selectedPatternName);
+ const selectedPattern = prebuiltPatterns.find(
+ (p) => p.name === selectedPatternName,
+ );
onPatternAdd({
id: `pattern-${Date.now()}`,
@@ -185,16 +193,21 @@ const ContentFilterConfiguration: React.FC = ({
setUploadValidating(true);
try {
const content = await file.text();
-
+
if (accessToken) {
const result = await validateBlockedWordsFile(accessToken, content);
if (result.valid) {
if (onFileUpload) {
onFileUpload(content);
}
- NotificationsManager.success(result.message || "File uploaded successfully");
+ NotificationsManager.success(
+ result.message || "File uploaded successfully",
+ );
} else {
- const errorMessage = result.error || (result.errors && result.errors.join(", ")) || "Invalid file";
+ const errorMessage =
+ result.error ||
+ (result.errors && result.errors.join(", ")) ||
+ "Invalid file";
NotificationsManager.error(`Validation failed: ${errorMessage}`);
}
}
@@ -203,48 +216,48 @@ const ContentFilterConfiguration: React.FC = ({
} finally {
setUploadValidating(false);
}
- return false;
};
const showPatterns = !showStep || showStep === "patterns";
const showKeywords = !showStep || showStep === "keywords";
const showCategories = !showStep || showStep === "categories";
const showCompetitorIntent =
- !showStep || showStep === "competitor_intent" || showStep === "categories";
+ !showStep ||
+ showStep === "competitor_intent" ||
+ showStep === "categories";
return (
{!showStep && (
-
- Configure patterns, keywords, and content categories to detect and filter sensitive information in requests and responses.
-
+
+ Configure patterns, keywords, and content categories to detect and
+ filter sensitive information in requests and responses.
+
)}
{showPatterns && (
-
-
- Pattern Detection
-
-
- Detect sensitive information using regex patterns (SSN, credit cards, API keys, etc.)
-
-
- }
- size="small"
- >
-
-
- setPatternModalVisible(true)} icon={ }>
- Add prebuilt pattern
-
- setCustomPatternModalVisible(true)} icon={ }>
- Add custom regex
-
-
+
+
+
Pattern Detection
+
+ Detect sensitive information using regex patterns (SSN, credit
+ cards, API keys, etc.)
+
+
+
+
setPatternModalVisible(true)}>
+
+ Add prebuilt pattern
+
+
setCustomPatternModalVisible(true)}
+ >
+
+ Add custom regex
+
= ({
)}
{showKeywords && (
-
-
- Blocked Keywords
-
-
- Block or mask specific sensitive terms and phrases
-
-
- }
- size="small"
- >
-
-
- setKeywordModalVisible(true)} icon={ }>
- Add keyword
-
-
- } loading={uploadValidating}>
- Upload YAML file
-
-
-
+
+
+
Blocked Keywords
+
+ Block or mask specific sensitive terms and phrases
+
+
+
+
setKeywordModalVisible(true)}>
+
+ Add keyword
+
+
{
+ const file = e.target.files?.[0];
+ if (file) {
+ void handleFileUpload(file);
+ }
+ if (e.target) {
+ e.target.value = "";
+ }
+ }}
+ />
+
fileInputRef.current?.click()}
+ >
+
+ {uploadValidating ? "Uploading..." : "Upload YAML file"}
+
= ({
)}
- {showCompetitorIntent &&
- onCompetitorIntentChange && (
-
- )}
-
- {showCategories && contentCategories.length > 0 && onContentCategoryAdd && onContentCategoryRemove && onContentCategoryUpdate && (
-
)}
+ {showCategories &&
+ contentCategories.length > 0 &&
+ onContentCategoryAdd &&
+ onContentCategoryRemove &&
+ onContentCategoryUpdate && (
+
+ )}
+
= ({
patternAction={customPatternAction}
onNameChange={setCustomPatternName}
onRegexChange={setCustomPatternRegex}
- onActionChange={(value) => setCustomPatternAction(value as "BLOCK" | "MASK")}
+ onActionChange={(value) =>
+ setCustomPatternAction(value as "BLOCK" | "MASK")
+ }
onAdd={handleAddCustomPattern}
onCancel={() => {
setCustomPatternModalVisible(false);
@@ -350,7 +379,9 @@ const ContentFilterConfiguration: React.FC = ({
action={newKeywordAction}
description={newKeywordDescription}
onKeywordChange={setNewKeyword}
- onActionChange={(value) => setNewKeywordAction(value as "BLOCK" | "MASK")}
+ onActionChange={(value) =>
+ setNewKeywordAction(value as "BLOCK" | "MASK")
+ }
onDescriptionChange={setNewKeywordDescription}
onAdd={handleAddKeyword}
onCancel={() => {
diff --git a/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentFilterManager.test.tsx b/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentFilterManager.test.tsx
index 76ff788afb8..3dc4428edf0 100644
--- a/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentFilterManager.test.tsx
+++ b/ui/litellm-dashboard/src/components/guardrails/content_filter/ContentFilterManager.test.tsx
@@ -99,26 +99,10 @@ vi.mock("./ContentFilterDisplay", () => ({
),
}));
-vi.mock("antd", async (importOriginal) => {
- const actual = await importOriginal();
- return {
- ...actual,
- Divider: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
- Alert: ({
- message,
- type,
- }: {
- message: React.ReactNode;
- type: string;
- }) => (
-
- {message}
-
- ),
- };
-});
+// Note: ContentFilterManager was migrated off antd Divider/Alert to a plain
+// inline section divider and a lucide AlertTriangle banner. The tests
+// below therefore assert against the semantic text of those elements rather
+// than the legacy `data-testid="divider"` / `"unsaved-alert"` hooks.
describe("ContentFilterManager", () => {
beforeEach(() => {
@@ -139,9 +123,7 @@ describe("ContentFilterManager", () => {
expect(screen.getByTestId("content-filter-config")).toBeInTheDocument();
});
- expect(screen.getByTestId("divider")).toHaveTextContent(
- "Content Filter Configuration"
- );
+ expect(screen.getByText("Content Filter Configuration")).toBeInTheDocument();
});
it("should return null when guardrail is not litellm_content_filter", () => {
@@ -299,16 +281,20 @@ describe("ContentFilterManager", () => {
expect(screen.getByTestId("content-filter-config")).toBeInTheDocument();
});
- expect(screen.queryByTestId("unsaved-alert")).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/You have unsaved changes/i),
+ ).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /add pattern/i }));
await waitFor(() => {
- expect(screen.getByTestId("unsaved-alert")).toBeInTheDocument();
+ expect(
+ screen.getByText(/You have unsaved changes/i),
+ ).toBeInTheDocument();
});
- expect(screen.getByTestId("unsaved-alert")).toHaveTextContent(
- /unsaved changes.*Save Changes/i
+ expect(screen.getByText(/You have unsaved changes/i)).toHaveTextContent(
+ /unsaved changes.*Save Changes/i,
);
});
@@ -399,7 +385,9 @@ describe("ContentFilterManager", () => {
);
await waitFor(() => {
- expect(screen.getByTestId("divider")).toBeInTheDocument();
+ expect(
+ screen.getByText("Content Filter Configuration"),
+ ).toBeInTheDocument();
});
expect(screen.queryByTestId("content-filter-config")).not.toBeInTheDocument();
diff --git a/ui/litellm-dashboard/src/components/guardrails/custom_code/CustomCodeModal.tsx b/ui/litellm-dashboard/src/components/guardrails/custom_code/CustomCodeModal.tsx
index d6e96c4d0cd..b798b70ba87 100644
--- a/ui/litellm-dashboard/src/components/guardrails/custom_code/CustomCodeModal.tsx
+++ b/ui/litellm-dashboard/src/components/guardrails/custom_code/CustomCodeModal.tsx
@@ -1,15 +1,49 @@
-import React, { useState, useRef, useEffect } from "react";
-import { Modal, Select, Switch, Collapse, Input, Divider } from "antd";
-// eslint-disable-next-line litellm-ui/no-banned-ui-imports
-import { Button, TextInput } from "@tremor/react";
-import { Code as CodeOutlined, PlayCircle as PlayCircleOutlined, CheckCircle2 as CheckCircleOutlined, XCircle as CloseCircleOutlined, ChevronRight as CaretRightOutlined, Save as SaveOutlined, UserRoundPlus as UsergroupAddOutlined, Upload as ExportOutlined } from "lucide-react";
-import { createGuardrailCall, updateGuardrailCall, testCustomCodeGuardrail } from "../../networking";
+import React, { useEffect, useRef, useState } from "react";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Separator } from "@/components/ui/separator";
+import { Switch } from "@/components/ui/switch";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Check as CheckCircleOutlined,
+ Code as CodeOutlined,
+ ExternalLink as ExportOutlined,
+ PlayCircle as PlayCircleOutlined,
+ Save as SaveOutlined,
+ UserRoundPlus as UsergroupAddOutlined,
+ X as CloseCircleOutlined,
+ X,
+} from "lucide-react";
+import {
+ createGuardrailCall,
+ testCustomCodeGuardrail,
+ updateGuardrailCall,
+} from "../../networking";
import NotificationsManager from "../../molecules/notifications_manager";
-const { Panel } = Collapse;
-const { TextArea } = Input;
-
-// Code templates
const CODE_TEMPLATES = {
empty: {
name: "Empty Template",
@@ -85,45 +119,79 @@ const CODE_TEMPLATES = {
},
};
-
-// Available primitives organized by category
const PRIMITIVES = {
"Return Values": [
{ name: "allow()", desc: "Let request/response through" },
{ name: "block(reason)", desc: "Reject with message" },
- { name: "modify(texts=[], images=[], tool_calls=[])", desc: "Transform content" },
+ {
+ name: "modify(texts=[], images=[], tool_calls=[])",
+ desc: "Transform content",
+ },
],
"HTTP Requests (async)": [
- { name: "await http_request(url, method, headers, body)", desc: "Make async HTTP request" },
+ {
+ name: "await http_request(url, method, headers, body)",
+ desc: "Make async HTTP request",
+ },
{ name: "await http_get(url, headers)", desc: "Async GET request" },
{ name: "await http_post(url, body, headers)", desc: "Async POST request" },
],
"Regex Functions": [
- { name: "regex_match(text, pattern)", desc: "Returns True if pattern found" },
- { name: "regex_replace(text, pattern, replacement)", desc: "Replace all matches" },
- { name: "regex_find_all(text, pattern)", desc: "Return list of matches" },
+ {
+ name: "regex_match(text, pattern)",
+ desc: "Returns True if pattern found",
+ },
+ {
+ name: "regex_replace(text, pattern, replacement)",
+ desc: "Replace all matches",
+ },
+ {
+ name: "regex_find_all(text, pattern)",
+ desc: "Return list of matches",
+ },
],
"JSON Functions": [
- { name: "json_parse(text)", desc: "Parse JSON string, returns None on error" },
+ {
+ name: "json_parse(text)",
+ desc: "Parse JSON string, returns None on error",
+ },
{ name: "json_stringify(obj)", desc: "Convert to JSON string" },
- { name: "json_schema_valid(obj, schema)", desc: "Validate against JSON schema" },
+ {
+ name: "json_schema_valid(obj, schema)",
+ desc: "Validate against JSON schema",
+ },
],
"URL Functions": [
{ name: "extract_urls(text)", desc: "Extract all URLs from text" },
{ name: "is_valid_url(url)", desc: "Check if URL is valid" },
- { name: "all_urls_valid(text)", desc: "Check all URLs in text are valid" },
+ {
+ name: "all_urls_valid(text)",
+ desc: "Check all URLs in text are valid",
+ },
],
"Code Detection": [
{ name: "detect_code(text)", desc: "Returns True if code detected" },
- { name: "detect_code_languages(text)", desc: "Returns list of detected languages" },
- { name: 'contains_code_language(text, ["sql"])', desc: "Check for specific languages" },
+ {
+ name: "detect_code_languages(text)",
+ desc: "Returns list of detected languages",
+ },
+ {
+ name: 'contains_code_language(text, ["sql"])',
+ desc: "Check for specific languages",
+ },
],
"Text Utilities": [
{ name: "contains(text, substring)", desc: "Check if substring exists" },
- { name: "contains_any(text, [substr1, substr2])", desc: "Check if any substring exists" },
+ {
+ name: "contains_any(text, [substr1, substr2])",
+ desc: "Check if any substring exists",
+ },
{ name: "word_count(text)", desc: "Count words" },
{ name: "char_count(text)", desc: "Count characters" },
- { name: "lower(text) / upper(text) / trim(text)", desc: "String transforms" },
+ {
+ name: "lower(text) / upper(text) / trim(text)",
+ desc: "String transforms",
+ },
],
};
@@ -137,7 +205,6 @@ const MODE_OPTIONS = [
{ value: "during_mcp_call", label: "during_mcp_call (During MCP Tool Call)" },
];
-// Data for editing an existing guardrail
export interface EditGuardrailData {
guardrail_id: string;
guardrail_name: string;
@@ -154,10 +221,74 @@ interface CustomCodeModalProps {
onClose: () => void;
onSuccess: () => void;
accessToken: string | null;
- /** If provided, the modal will be in edit mode */
editData?: EditGuardrailData | null;
}
+/**
+ * Chip-style multi-select for Mode options. Mirrors the multi-select
+ * pattern used elsewhere in the migrated UI.
+ */
+function ModeMultiSelect({
+ value,
+ onChange,
+}: {
+ value: string[];
+ onChange: (next: string[]) => void;
+}) {
+ const remaining = MODE_OPTIONS.filter((o) => !value.includes(o.value));
+ return (
+
+
{
+ if (v) onChange([...value, v]);
+ }}
+ >
+
+
+
+
+ {remaining.length === 0 ? (
+
+ All modes selected
+
+ ) : (
+ remaining.map((opt) => (
+
+ {opt.label}
+
+ ))
+ )}
+
+
+ {value.length > 0 && (
+
+ {value.map((v) => {
+ const opt = MODE_OPTIONS.find((o) => o.value === v);
+ return (
+
+ {opt?.label ?? v}
+ onChange(value.filter((s) => s !== v))}
+ className="inline-flex items-center justify-center rounded-full hover:bg-muted-foreground/20"
+ aria-label={`Remove ${opt?.label ?? v}`}
+ >
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
const CustomCodeModal: React.FC = ({
visible,
onClose,
@@ -174,8 +305,7 @@ const CustomCodeModal: React.FC = ({
const [isSaving, setIsSaving] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const [testExpanded, setTestExpanded] = useState(false);
-
- // Test input examples for pre_call and post_call
+
const TEST_INPUT_EXAMPLES = {
pre_call: {
name: "Pre-call (Request)",
@@ -191,20 +321,20 @@ const CustomCodeModal: React.FC = ({
parameters: {
type: "object",
properties: {
- location: { type: "string", description: "City name" }
+ location: { type: "string", description: "City name" },
},
- required: ["location"]
- }
- }
- }
+ required: ["location"],
+ },
+ },
+ },
],
tool_calls: [],
structured_messages: [
{ role: "system", content: "You are a helpful assistant." },
- { role: "user", content: "Hello, my SSN is 123-45-6789" }
+ { role: "user", content: "Hello, my SSN is 123-45-6789" },
],
- model: "gpt-4"
- }
+ model: "gpt-4",
+ },
},
post_call: {
name: "Post-call (Response)",
@@ -218,19 +348,19 @@ const CustomCodeModal: React.FC = ({
type: "function",
function: {
name: "get_weather",
- arguments: "{\"location\": \"San Francisco\"}"
- }
- }
+ arguments: '{"location": "San Francisco"}',
+ },
+ },
],
structured_messages: [],
- model: "gpt-4"
- }
+ model: "gpt-4",
+ },
},
pre_mcp_call: {
name: "Pre MCP (MCP tool as OpenAI tool)",
data: {
texts: [
- "Tool: read_wiki_structure\nArguments: {\"repoName\": \"BerriAI/litellm\"}"
+ 'Tool: read_wiki_structure\nArguments: {"repoName": "BerriAI/litellm"}',
],
images: [],
tools: [
@@ -238,16 +368,20 @@ const CustomCodeModal: React.FC = ({
type: "function",
function: {
name: "read_wiki_structure",
- description: "Read the structure of a GitHub repository (MCP tool passed as OpenAI tool)",
+ description:
+ "Read the structure of a GitHub repository (MCP tool passed as OpenAI tool)",
parameters: {
type: "object",
properties: {
- repoName: { type: "string", description: "Repository name, e.g. BerriAI/litellm" }
+ repoName: {
+ type: "string",
+ description: "Repository name, e.g. BerriAI/litellm",
+ },
},
- required: ["repoName"]
- }
- }
- }
+ required: ["repoName"],
+ },
+ },
+ },
],
tool_calls: [
{
@@ -255,50 +389,53 @@ const CustomCodeModal: React.FC = ({
type: "function",
function: {
name: "read_wiki_structure",
- arguments: "{\"repoName\": \"BerriAI/litellm\"}"
- }
- }
+ arguments: '{"repoName": "BerriAI/litellm"}',
+ },
+ },
],
structured_messages: [
- { role: "user", content: "Tool: read_wiki_structure\nArguments: {\"repoName\": \"BerriAI/litellm\"}" }
+ {
+ role: "user",
+ content:
+ 'Tool: read_wiki_structure\nArguments: {"repoName": "BerriAI/litellm"}',
+ },
],
- model: "mcp-tool-call"
- }
- }
+ model: "mcp-tool-call",
+ },
+ },
};
-
- const [testInput, setTestInput] = useState(JSON.stringify(TEST_INPUT_EXAMPLES.pre_call.data, null, 2));
+
+ const [testInput, setTestInput] = useState(
+ JSON.stringify(TEST_INPUT_EXAMPLES.pre_call.data, null, 2),
+ );
const [testResult, setTestResult] = useState(null);
const [copiedPrimitive, setCopiedPrimitive] = useState(null);
const textareaRef = useRef(null);
- // Handle template change
const handleTemplateChange = (templateKey: string) => {
setSelectedTemplate(templateKey);
-
- // Check if it's a standard template
- setCode(CODE_TEMPLATES[templateKey as keyof typeof CODE_TEMPLATES].code);
+ setCode(
+ CODE_TEMPLATES[templateKey as keyof typeof CODE_TEMPLATES].code,
+ );
};
- // Normalize mode from API (string or string[]) to string[]
const normalizeMode = (m: string | string[] | undefined): string[] => {
if (m === undefined || m === null) return ["pre_call"];
if (Array.isArray(m)) return m.length ? m : ["pre_call"];
return [m];
};
- // Reset form when modal opens or editData changes
useEffect(() => {
if (visible) {
if (editData) {
- // Edit mode: populate with existing data
setGuardrailName(editData.guardrail_name || "");
setMode(normalizeMode(editData.litellm_params?.mode));
setDefaultOn(editData.litellm_params?.default_on || false);
- setCode(editData.litellm_params?.custom_code || CODE_TEMPLATES.empty.code);
- setSelectedTemplate(""); // No template selected in edit mode
+ setCode(
+ editData.litellm_params?.custom_code || CODE_TEMPLATES.empty.code,
+ );
+ setSelectedTemplate("");
} else {
- // Create mode: reset to defaults
setGuardrailName("");
setMode(["pre_call"]);
setDefaultOn(false);
@@ -310,7 +447,6 @@ const CustomCodeModal: React.FC = ({
}
}, [visible, editData]);
- // Copy primitive to clipboard
const copyPrimitive = async (primitive: string) => {
try {
await navigator.clipboard.writeText(primitive);
@@ -321,14 +457,14 @@ const CustomCodeModal: React.FC = ({
}
};
- // Handle tab key in textarea
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Tab") {
e.preventDefault();
const textarea = e.currentTarget;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
- const newValue = code.substring(0, start) + " " + code.substring(end);
+ const newValue =
+ code.substring(0, start) + " " + code.substring(end);
setCode(newValue);
setTimeout(() => {
textarea.selectionStart = textarea.selectionEnd = start + 4;
@@ -336,7 +472,6 @@ const CustomCodeModal: React.FC = ({
}
};
- // Save guardrail (create or update)
const handleSave = async () => {
if (!guardrailName.trim()) {
NotificationsManager.fromBackend("Please enter a guardrail name");
@@ -354,14 +489,12 @@ const CustomCodeModal: React.FC = ({
setIsSaving(true);
try {
if (isEditMode && editData) {
- // Update existing guardrail
const updateData: any = {
litellm_params: {
custom_code: code,
},
};
- // Only include changed fields
if (guardrailName !== editData.guardrail_name) {
updateData.guardrail_name = guardrailName;
}
@@ -377,9 +510,10 @@ const CustomCodeModal: React.FC = ({
}
await updateGuardrailCall(accessToken, editData.guardrail_id, updateData);
- NotificationsManager.success("Custom code guardrail updated successfully");
+ NotificationsManager.success(
+ "Custom code guardrail updated successfully",
+ );
} else {
- // Create new guardrail
const guardrailData = {
guardrail_name: guardrailName,
litellm_params: {
@@ -392,21 +526,23 @@ const CustomCodeModal: React.FC = ({
};
await createGuardrailCall(accessToken, guardrailData);
- NotificationsManager.success("Custom code guardrail created successfully");
+ NotificationsManager.success(
+ "Custom code guardrail created successfully",
+ );
}
onSuccess();
onClose();
} catch (error) {
console.error("Failed to save guardrail:", error);
NotificationsManager.fromBackend(
- `Failed to ${isEditMode ? "update" : "create"} guardrail: ` + (error instanceof Error ? error.message : String(error))
+ `Failed to ${isEditMode ? "update" : "create"} guardrail: ` +
+ (error instanceof Error ? error.message : String(error)),
);
} finally {
setIsSaving(false);
}
};
- // Test guardrail using backend endpoint
const handleTest = async () => {
if (!accessToken) {
setTestResult({ error: "No access token available" });
@@ -417,7 +553,6 @@ const CustomCodeModal: React.FC = ({
setTestResult(null);
try {
- // Parse test input JSON
let parsedInput;
try {
parsedInput = JSON.parse(testInput);
@@ -427,20 +562,19 @@ const CustomCodeModal: React.FC = ({
return;
}
- // Ensure texts array exists
if (!parsedInput.texts) {
parsedInput.texts = [];
}
- // Use first request-like or response-like mode for test input_type
const requestModes = ["pre_call", "pre_mcp_call"];
const responseModes = ["post_call", "post_mcp_call"];
- const testInputType: "request" | "response" =
- mode.some((m) => requestModes.includes(m))
- ? "request"
- : mode.some((m) => responseModes.includes(m))
- ? "response"
- : "request";
+ const testInputType: "request" | "response" = mode.some((m) =>
+ requestModes.includes(m),
+ )
+ ? "request"
+ : mode.some((m) => responseModes.includes(m))
+ ? "response"
+ : "request";
const response = await testCustomCodeGuardrail(accessToken, {
custom_code: code,
@@ -465,7 +599,8 @@ const CustomCodeModal: React.FC = ({
} catch (error) {
console.error("Failed to test custom code:", error);
setTestResult({
- error: error instanceof Error ? error.message : "Failed to test custom code",
+ error:
+ error instanceof Error ? error.message : "Failed to test custom code",
});
} finally {
setIsTesting(false);
@@ -475,350 +610,423 @@ const CustomCodeModal: React.FC = ({
const lineCount = code.split("\n").length;
return (
- {
+ if (!open) onClose();
+ }}
>
-
- {/* Header */}
-
-
+
+
+
{isEditMode ? "Edit Custom Guardrail" : "Create Custom Guardrail"}
-
-
Define custom logic using Python-like syntax
-
-
- {/* Top Controls */}
-
-
- Guardrail Name
-
-
-
- Mode (can select multiple)
-
-
-
-
Template
-
(
- <>
- {menu}
-
- {
- e.preventDefault();
- window.open('https://models.litellm.ai/guardrails', '_blank');
- }}
- onMouseEnter={(e) => {
- e.currentTarget.style.backgroundColor = '#f0f0f0';
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.backgroundColor = 'transparent';
- }}
- >
-
- Browse Community templates
-
-
- >
- )}
- >
-
- {Object.entries(CODE_TEMPLATES).map(([key, template]) => (
-
- {template.name}
-
- ))}
-
-
-
-
- Default On
-
-
-
-
- {/* Main Content */}
-
- {/* Code Editor */}
-
-
- Python Logic
- Restricted environment (no imports)
-
-
- {/* Line numbers */}
-
- {Array.from({ length: Math.max(lineCount, 20) }, (_, i) => (
-
{i + 1}
- ))}
-
- {/* Code textarea */}
-