mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix: fix styling
This commit is contained in:
parent
4e62386c65
commit
a60122265b
1 changed files with 43 additions and 289 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { Modal, Select, Switch, Collapse, Input, Divider } from "antd";
|
||||
import { Modal, Select, Switch, Collapse, Input, Spin } from "antd";
|
||||
import { Button, TextInput } from "@tremor/react";
|
||||
import {
|
||||
CodeOutlined,
|
||||
|
|
@ -8,10 +8,8 @@ import {
|
|||
CloseCircleOutlined,
|
||||
CaretRightOutlined,
|
||||
SaveOutlined,
|
||||
UsergroupAddOutlined,
|
||||
ExportOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { createGuardrailCall, updateGuardrailCall, testCustomCodeGuardrail } from "../../networking";
|
||||
import { createGuardrailCall, testCustomCodeGuardrail } from "../../networking";
|
||||
import NotificationsManager from "../../molecules/notifications_manager";
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
|
@ -21,7 +19,7 @@ const { TextArea } = Input;
|
|||
const CODE_TEMPLATES = {
|
||||
empty: {
|
||||
name: "Empty Template",
|
||||
code: `async def apply_guardrail(inputs, request_data, input_type):
|
||||
code: `def apply_guardrail(inputs, request_data, input_type):
|
||||
# inputs: {texts, images, tools, tool_calls, structured_messages, model}
|
||||
# request_data: {model, user_id, team_id, end_user_id, metadata}
|
||||
# input_type: "request" or "response"
|
||||
|
|
@ -70,30 +68,8 @@ const CODE_TEMPLATES = {
|
|||
return block("Response missing required fields")
|
||||
return allow()`,
|
||||
},
|
||||
externalAPI: {
|
||||
name: "External API Check (async)",
|
||||
code: `async def apply_guardrail(inputs, request_data, input_type):
|
||||
# Call an external moderation API (async for non-blocking)
|
||||
for text in inputs["texts"]:
|
||||
response = await http_post(
|
||||
"https://api.example.com/moderate",
|
||||
body={"text": text, "user_id": request_data["user_id"]},
|
||||
headers={"Authorization": "Bearer YOUR_API_KEY"},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if not response["success"]:
|
||||
# API call failed, allow by default or block
|
||||
return allow()
|
||||
|
||||
if response["body"].get("flagged"):
|
||||
return block(response["body"].get("reason", "Content flagged"))
|
||||
|
||||
return allow()`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// Available primitives organized by category
|
||||
const PRIMITIVES = {
|
||||
"Return Values": [
|
||||
|
|
@ -101,11 +77,6 @@ const PRIMITIVES = {
|
|||
{ name: "block(reason)", desc: "Reject with message" },
|
||||
{ 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_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" },
|
||||
|
|
@ -140,30 +111,13 @@ const MODE_OPTIONS = [
|
|||
{ value: "post_call", label: "post_call (Response)" },
|
||||
{ value: "during_call", label: "during_call (Parallel)" },
|
||||
{ value: "logging_only", label: "logging_only" },
|
||||
{ value: "pre_mcp_call", label: "pre_mcp_call (Before MCP Tool Call)" },
|
||||
{ value: "post_mcp_call", label: "post_mcp_call (After MCP Tool Call)" },
|
||||
{ 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;
|
||||
litellm_params: {
|
||||
mode?: string | string[];
|
||||
default_on?: boolean;
|
||||
custom_code?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface CustomCodeModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
accessToken: string | null;
|
||||
/** If provided, the modal will be in edit mode */
|
||||
editData?: EditGuardrailData | null;
|
||||
}
|
||||
|
||||
const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
||||
|
|
@ -171,72 +125,16 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
onClose,
|
||||
onSuccess,
|
||||
accessToken,
|
||||
editData,
|
||||
}) => {
|
||||
const isEditMode = !!editData;
|
||||
const [guardrailName, setGuardrailName] = useState("");
|
||||
const [mode, setMode] = useState<string[]>(["pre_call"]);
|
||||
const [mode, setMode] = useState<string>("pre_call");
|
||||
const [defaultOn, setDefaultOn] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>("empty");
|
||||
const [code, setCode] = useState(CODE_TEMPLATES.empty.code);
|
||||
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)",
|
||||
data: {
|
||||
texts: ["Hello, my SSN is 123-45-6789"],
|
||||
images: [],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get the current weather in a location",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string", description: "City name" }
|
||||
},
|
||||
required: ["location"]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
tool_calls: [],
|
||||
structured_messages: [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ role: "user", content: "Hello, my SSN is 123-45-6789" }
|
||||
],
|
||||
model: "gpt-4"
|
||||
}
|
||||
},
|
||||
post_call: {
|
||||
name: "Post-call (Response)",
|
||||
data: {
|
||||
texts: ["The weather in San Francisco is 72°F and sunny."],
|
||||
images: [],
|
||||
tools: [],
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_abc123",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_weather",
|
||||
arguments: "{\"location\": \"San Francisco\"}"
|
||||
}
|
||||
}
|
||||
],
|
||||
structured_messages: [],
|
||||
model: "gpt-4"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [testInput, setTestInput] = useState(JSON.stringify(TEST_INPUT_EXAMPLES.pre_call.data, null, 2));
|
||||
const [testInput, setTestInput] = useState('{"texts": ["Hello, my SSN is 123-45-6789"], "images": [], "tools": [], "tool_calls": [], "structured_messages": [], "model": "gpt-4"}');
|
||||
const [testResult, setTestResult] = useState<any>(null);
|
||||
const [copiedPrimitive, setCopiedPrimitive] = useState<string | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
|
@ -244,40 +142,21 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
// 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);
|
||||
};
|
||||
|
||||
// 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
|
||||
// Reset form when modal opens
|
||||
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
|
||||
} else {
|
||||
// Create mode: reset to defaults
|
||||
setGuardrailName("");
|
||||
setMode(["pre_call"]);
|
||||
setDefaultOn(false);
|
||||
setSelectedTemplate("empty");
|
||||
setCode(CODE_TEMPLATES.empty.code);
|
||||
}
|
||||
setGuardrailName("");
|
||||
setMode("pre_call");
|
||||
setDefaultOn(false);
|
||||
setSelectedTemplate("empty");
|
||||
setCode(CODE_TEMPLATES.empty.code);
|
||||
setTestResult(null);
|
||||
setTestExpanded(false);
|
||||
}
|
||||
}, [visible, editData]);
|
||||
}, [visible]);
|
||||
|
||||
// Copy primitive to clipboard
|
||||
const copyPrimitive = async (primitive: string) => {
|
||||
|
|
@ -305,7 +184,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
// Save guardrail (create or update)
|
||||
// Save guardrail
|
||||
const handleSave = async () => {
|
||||
if (!guardrailName.trim()) {
|
||||
NotificationsManager.fromBackend("Please enter a guardrail name");
|
||||
|
|
@ -322,53 +201,25 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
if (isEditMode && editData) {
|
||||
// Update existing guardrail
|
||||
const updateData: any = {
|
||||
litellm_params: {
|
||||
custom_code: code,
|
||||
},
|
||||
};
|
||||
const guardrailData = {
|
||||
guardrail_name: guardrailName,
|
||||
litellm_params: {
|
||||
guardrail: "custom_code",
|
||||
mode: mode,
|
||||
default_on: defaultOn,
|
||||
custom_code: code,
|
||||
},
|
||||
guardrail_info: {},
|
||||
};
|
||||
|
||||
// Only include changed fields
|
||||
if (guardrailName !== editData.guardrail_name) {
|
||||
updateData.guardrail_name = guardrailName;
|
||||
}
|
||||
const existingMode = normalizeMode(editData.litellm_params?.mode);
|
||||
const modeChanged =
|
||||
mode.length !== existingMode.length ||
|
||||
mode.some((m, i) => m !== existingMode[i]);
|
||||
if (modeChanged) {
|
||||
updateData.litellm_params.mode = mode;
|
||||
}
|
||||
if (defaultOn !== editData.litellm_params?.default_on) {
|
||||
updateData.litellm_params.default_on = defaultOn;
|
||||
}
|
||||
|
||||
await updateGuardrailCall(accessToken, editData.guardrail_id, updateData);
|
||||
NotificationsManager.success("Custom code guardrail updated successfully");
|
||||
} else {
|
||||
// Create new guardrail
|
||||
const guardrailData = {
|
||||
guardrail_name: guardrailName,
|
||||
litellm_params: {
|
||||
guardrail: "custom_code",
|
||||
mode: mode,
|
||||
default_on: defaultOn,
|
||||
custom_code: code,
|
||||
},
|
||||
guardrail_info: {},
|
||||
};
|
||||
|
||||
await createGuardrailCall(accessToken, guardrailData);
|
||||
NotificationsManager.success("Custom code guardrail created successfully");
|
||||
}
|
||||
await createGuardrailCall(accessToken, guardrailData);
|
||||
NotificationsManager.success("Custom code guardrail created successfully");
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to save guardrail:", error);
|
||||
console.error("Failed to create guardrail:", error);
|
||||
NotificationsManager.fromBackend(
|
||||
`Failed to ${isEditMode ? "update" : "create"} guardrail: ` + (error instanceof Error ? error.message : String(error))
|
||||
"Failed to create guardrail: " + (error instanceof Error ? error.message : String(error))
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
|
|
@ -401,20 +252,10 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
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 response = await testCustomCodeGuardrail(accessToken, {
|
||||
custom_code: code,
|
||||
test_input: parsedInput,
|
||||
input_type: testInputType,
|
||||
input_type: mode as "request" | "response",
|
||||
request_data: {
|
||||
model: "test-model",
|
||||
metadata: {},
|
||||
|
|
@ -456,9 +297,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
<div className="flex flex-col h-[80vh]">
|
||||
{/* Header */}
|
||||
<div className="pb-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
{isEditMode ? "Edit Custom Guardrail" : "Create Custom Guardrail"}
|
||||
</h2>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Create Custom Guardrail</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Define custom logic using Python-like syntax</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -472,16 +311,14 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
placeholder="e.g., block-pii-custom"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[280px]">
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Mode (can select multiple)</label>
|
||||
<div className="w-[180px]">
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Mode</label>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={mode}
|
||||
onChange={setMode}
|
||||
options={MODE_OPTIONS}
|
||||
className="w-full"
|
||||
size="middle"
|
||||
placeholder="Select modes"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[180px]">
|
||||
|
|
@ -491,45 +328,12 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
onChange={handleTemplateChange}
|
||||
className="w-full"
|
||||
size="middle"
|
||||
dropdownRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
cursor: 'pointer',
|
||||
color: '#1890ff',
|
||||
fontSize: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.open('https://models.litellm.ai/guardrails', '_blank');
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#f0f0f0';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
}}
|
||||
>
|
||||
<UsergroupAddOutlined />
|
||||
<span>Browse Community templates</span>
|
||||
<ExportOutlined style={{ fontSize: '10px' }} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<Select.OptGroup label="STANDARD">
|
||||
{Object.entries(CODE_TEMPLATES).map(([key, template]) => (
|
||||
<Select.Option key={key} value={key}>
|
||||
{template.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select.OptGroup>
|
||||
{Object.entries(CODE_TEMPLATES).map(([key, template]) => (
|
||||
<Select.Option key={key} value={key}>
|
||||
{template.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pt-5">
|
||||
|
|
@ -541,12 +345,12 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
{/* Main Content */}
|
||||
<div className="flex flex-1 overflow-hidden mt-4 gap-6">
|
||||
{/* Code Editor */}
|
||||
<div className="flex-[2] flex flex-col min-w-0 overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-2 flex-shrink-0">
|
||||
<div className="flex-[2] flex flex-col min-w-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Python Logic</span>
|
||||
<span className="text-xs text-gray-400">Restricted environment (no imports)</span>
|
||||
</div>
|
||||
<div className="relative rounded-lg overflow-hidden border border-gray-700 bg-[#1e1e1e] flex-shrink-0" style={{ minHeight: "300px", maxHeight: "400px" }}>
|
||||
<div className="flex-1 relative rounded-lg overflow-hidden border border-gray-700 bg-[#1e1e1e]" style={{ minHeight: "350px" }}>
|
||||
{/* Line numbers */}
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-12 bg-[#1e1e1e] border-r border-gray-700 text-right pr-3 pt-3 select-none overflow-hidden"
|
||||
|
|
@ -572,7 +376,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
<Collapse
|
||||
activeKey={testExpanded ? ["test"] : []}
|
||||
onChange={(keys) => setTestExpanded(keys.includes("test"))}
|
||||
className="mt-3 bg-white border border-gray-200 rounded-lg flex-shrink-0"
|
||||
className="mt-3 bg-white border border-gray-200 rounded-lg"
|
||||
expandIcon={({ isActive }) => <CaretRightOutlined rotate={isActive ? 90 : 0} />}
|
||||
>
|
||||
<Panel
|
||||
|
|
@ -586,40 +390,11 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="block text-xs font-medium text-gray-600">Test Input (JSON)</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500">Load example:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTestInput(JSON.stringify(TEST_INPUT_EXAMPLES.pre_call.data, null, 2))}
|
||||
className="px-2 py-1 text-xs rounded border border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100 transition-colors"
|
||||
>
|
||||
Pre-call
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTestInput(JSON.stringify(TEST_INPUT_EXAMPLES.post_call.data, null, 2))}
|
||||
className="px-2 py-1 text-xs rounded border border-green-200 bg-green-50 text-green-700 hover:bg-green-100 transition-colors"
|
||||
>
|
||||
Post-call
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2 p-2 bg-gray-50 rounded text-xs text-gray-600 border border-gray-200">
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
|
||||
<div><strong>texts</strong>: Message content (always)</div>
|
||||
<div><strong>images</strong>: Base64 images (vision)</div>
|
||||
<div><strong>tools</strong>: Tool definitions <span className="text-orange-600">(pre_call)</span></div>
|
||||
<div><strong>tool_calls</strong>: LLM tool calls <span className="text-green-600">(post_call)</span></div>
|
||||
<div><strong>structured_messages</strong>: Full messages <span className="text-orange-600">(pre_call)</span></div>
|
||||
<div><strong>model</strong>: Model name (always)</div>
|
||||
</div>
|
||||
</div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Test Input (JSON)</label>
|
||||
<TextArea
|
||||
value={testInput}
|
||||
onChange={(e) => setTestInput(e.target.value)}
|
||||
rows={8}
|
||||
rows={4}
|
||||
className="font-mono text-xs"
|
||||
placeholder='{"texts": ["test message"], ...}'
|
||||
/>
|
||||
|
|
@ -670,27 +445,6 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
</div>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
{/* Contribution CTA Banner */}
|
||||
<div className="mt-3 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg flex items-center justify-between flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-blue-100 rounded-full p-2">
|
||||
<UsergroupAddOutlined className="text-blue-600 text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">Built a useful guardrail?</div>
|
||||
<div className="text-xs text-gray-600">Share it with the community and help others build faster</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
onClick={() => window.open('https://github.com/BerriAI/litellm-guardrails', '_blank')}
|
||||
icon={ExportOutlined}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white border-0"
|
||||
>
|
||||
Contribute Template
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Primitives Panel */}
|
||||
|
|
@ -755,7 +509,7 @@ const CustomCodeModal: React.FC<CustomCodeModalProps> = ({
|
|||
disabled={isSaving || !guardrailName.trim()}
|
||||
icon={SaveOutlined}
|
||||
>
|
||||
{isEditMode ? "Update Guardrail" : "Save Guardrail"}
|
||||
Save Guardrail
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue