-
Test Configuration
-
+
+
+ Test Configuration
+
+
Re-run this guardrail on recent failing logs to validate your changes
}
- loading={rerunStatus === "running"}
onClick={handleRerun}
+ disabled={rerunStatus === "running"}
>
- {rerunStatus === "running" ? "Running on 10 samples..." : "Re-run on failing logs"}
+ {rerunStatus !== "running" &&
}
+ {rerunStatus === "running"
+ ? "Running on 10 samples..."
+ : "Re-run on failing logs"}
{rerunStatus === "success" && (
-
- 7/10 would now pass with new config
+
+ 7/10 would now pass with new config
)}
{rerunStatus === "error" && (
- Error running tests
+ Error running tests
)}
diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/GuardrailDetail.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/GuardrailDetail.tsx
index 1f77c8e3db6..aec73c68df6 100644
--- a/ui/litellm-dashboard/src/components/GuardrailsMonitor/GuardrailDetail.tsx
+++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/GuardrailDetail.tsx
@@ -1,11 +1,8 @@
-import {
- ArrowLeftOutlined,
- SafetyOutlined,
- SettingOutlined,
- WarningOutlined,
-} from "@ant-design/icons";
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useQuery } from "@tanstack/react-query";
-import { Button, Col, Row, Spin, Tabs } from "antd";
+import { ArrowLeft, Settings, Shield, AlertTriangle } from "lucide-react";
import React, { useMemo, useState } from "react";
import {
getGuardrailsUsageDetail,
@@ -24,13 +21,25 @@ interface GuardrailDetailProps {
endDate: string;
}
-const statusColors: Record<
+const statusStyles: Record<
string,
{ bg: string; text: string; dot: string }
> = {
- healthy: { bg: "bg-green-50", text: "text-green-700", dot: "bg-green-500" },
- warning: { bg: "bg-amber-50", text: "text-amber-700", dot: "bg-amber-500" },
- critical: { bg: "bg-red-50", text: "text-red-700", dot: "bg-red-500" },
+ healthy: {
+ bg: "bg-emerald-50 dark:bg-emerald-950/30",
+ text: "text-emerald-700 dark:text-emerald-400",
+ dot: "bg-emerald-500",
+ },
+ warning: {
+ bg: "bg-amber-50 dark:bg-amber-950/30",
+ text: "text-amber-700 dark:text-amber-400",
+ dot: "bg-amber-500",
+ },
+ critical: {
+ bg: "bg-red-50 dark:bg-red-950/30",
+ text: "text-red-700 dark:text-red-400",
+ dot: "bg-red-500",
+ },
};
export function GuardrailDetail({
@@ -42,12 +51,17 @@ export function GuardrailDetail({
}: GuardrailDetailProps) {
const [activeTab, setActiveTab] = useState("overview");
const [evaluationModalOpen, setEvaluationModalOpen] = useState(false);
- const [logsPage, setLogsPage] = useState(1);
+ const [logsPage] = useState(1);
const logsPageSize = 50;
- const { data: detailData, isLoading: detailLoading, error: detailError } = useQuery({
+ const {
+ data: detailData,
+ isLoading: detailLoading,
+ error: detailError,
+ } = useQuery({
queryKey: ["guardrails-usage-detail", guardrailId, startDate, endDate],
- queryFn: () => getGuardrailsUsageDetail(accessToken!, guardrailId, startDate, endDate),
+ queryFn: () =>
+ getGuardrailsUsageDetail(accessToken!, guardrailId, startDate, endDate),
enabled: !!accessToken && !!guardrailId,
});
const { data: logsData, isLoading: logsLoading } = useQuery({
@@ -100,22 +114,23 @@ export function GuardrailDetail({
avgScore: undefined as number | undefined,
avgLatency: undefined as number | undefined,
};
- const statusStyle = statusColors[data.status] ?? statusColors.healthy;
+ const statusStyle = statusStyles[data.status] ?? statusStyles.healthy;
if (detailLoading && !detailData) {
return (
-
+
);
}
if (detailError && !detailData) {
return (
-
} onClick={onBack} className="pl-0 mb-4">
+
-
Failed to load guardrail details.
+
Failed to load guardrail details.
);
}
@@ -123,86 +138,97 @@ export function GuardrailDetail({
return (
-
}
- onClick={onBack}
- className="pl-0 mb-4"
- >
+
-
-
{data.name}
+
+
+ {data.name}
+
-
+
{data.status.charAt(0).toUpperCase() + data.status.slice(1)}
-
{data.description}
+
+ {data.description}
+
-
+
{data.provider}
}
+ variant="outline"
+ size="icon"
onClick={() => setEvaluationModalOpen(true)}
title="Evaluation settings"
- />
+ >
+
+
-
+
+
+ Overview
+ Logs
+
+
{activeTab === "overview" && (
-
-
-
-
-
- 15 ? "text-red-600" : data.failRate > 5 ? "text-amber-600" : "text-green-600"
- }
- subtitle={`${Math.round((data.requestsEvaluated * data.failRate) / 100).toLocaleString()} blocked`}
- icon={data.failRate > 15 ? : undefined}
- />
-
-
- 150
- ? "text-red-600"
- : data.avgLatency > 50
- ? "text-amber-600"
- : "text-green-600"
- : "text-gray-500"
- }
- subtitle={data.avgLatency != null ? "Per request (avg)" : "No data"}
- />
-
-
+
+
+
15
+ ? "text-red-600 dark:text-red-400"
+ : data.failRate > 5
+ ? "text-amber-600 dark:text-amber-400"
+ : "text-emerald-600 dark:text-emerald-400"
+ }
+ subtitle={`${Math.round((data.requestsEvaluated * data.failRate) / 100).toLocaleString()} blocked`}
+ icon={
+ data.failRate > 15 ? (
+
+ ) : undefined
+ }
+ />
+ 150
+ ? "text-red-600 dark:text-red-400"
+ : data.avgLatency > 50
+ ? "text-amber-600 dark:text-amber-400"
+ : "text-emerald-600 dark:text-emerald-400"
+ : "text-muted-foreground"
+ }
+ subtitle={data.avgLatency != null ? "Per request (avg)" : "No data"}
+ />
+
= {
- Bedrock: "bg-orange-100 text-orange-700 border-orange-200",
- "Google Cloud": "bg-sky-100 text-sky-700 border-sky-200",
- LiteLLM: "bg-indigo-100 text-indigo-700 border-indigo-200",
- Custom: "bg-gray-100 text-gray-600 border-gray-200",
+ Bedrock:
+ "bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-950/30 dark:text-orange-300 dark:border-orange-800",
+ "Google Cloud":
+ "bg-sky-100 text-sky-700 border-sky-200 dark:bg-sky-950/30 dark:text-sky-300 dark:border-sky-800",
+ LiteLLM:
+ "bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-950/30 dark:text-indigo-300 dark:border-indigo-800",
+ Custom: "bg-muted text-muted-foreground border-border",
};
function computeMetricsFromRows(data: PerformanceRow[]) {
const totalRequests = data.reduce((sum, r) => sum + r.requestsEvaluated, 0);
const totalBlocked = data.reduce(
(sum, r) => sum + Math.round((r.requestsEvaluated * r.failRate) / 100),
- 0
+ 0,
);
const passRate =
- totalRequests > 0 ? ((1 - totalBlocked / totalRequests) * 100).toFixed(1) : "0";
+ totalRequests > 0
+ ? ((1 - totalBlocked / totalRequests) * 100).toFixed(1)
+ : "0";
const withLat = data.filter((r) => r.avgLatency != null);
const avgLatency =
withLat.length > 0
- ? Math.round(withLat.reduce((sum, r) => sum + (r.avgLatency ?? 0), 0) / withLat.length)
+ ? Math.round(
+ withLat.reduce((sum, r) => sum + (r.avgLatency ?? 0), 0) /
+ withLat.length,
+ )
: 0;
- return { totalRequests, totalBlocked, passRate, avgLatency, count: data.length };
+ return {
+ totalRequests,
+ totalBlocked,
+ passRate,
+ avgLatency,
+ count: data.length,
+ };
}
export function GuardrailsOverview({
@@ -62,7 +85,11 @@ export function GuardrailsOverview({
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
const [evaluationModalOpen, setEvaluationModalOpen] = useState(false);
- const { data: guardrailsData, isLoading: guardrailsLoading, error: guardrailsError } = useQuery({
+ const {
+ data: guardrailsData,
+ isLoading: guardrailsLoading,
+ error: guardrailsError,
+ } = useQuery({
queryKey: ["guardrails-usage-overview", startDate, endDate],
queryFn: () => getGuardrailsUsageOverview(accessToken!, startDate, endDate),
enabled: !!accessToken,
@@ -75,7 +102,12 @@ export function GuardrailsOverview({
totalRequests: guardrailsData.totalRequests ?? 0,
totalBlocked: guardrailsData.totalBlocked ?? 0,
passRate: String(guardrailsData.passRate ?? 0),
- avgLatency: activeData.length ? Math.round(activeData.reduce((s, r) => s + (r.avgLatency ?? 0), 0) / activeData.length) : 0,
+ avgLatency: activeData.length
+ ? Math.round(
+ activeData.reduce((s, r) => s + (r.avgLatency ?? 0), 0) /
+ activeData.length,
+ )
+ : 0,
count: activeData.length,
};
}
@@ -93,108 +125,12 @@ export function GuardrailsOverview({
const isLoading = guardrailsLoading;
const error = guardrailsError;
- const columns: ColumnsType = [
- {
- title: "Guardrail",
- dataIndex: "name",
- key: "name",
- render: (name: string, row) => (
-
- ),
- },
- {
- title: "Provider",
- dataIndex: "provider",
- key: "provider",
- render: (provider: string) => (
-
- {provider}
-
- ),
- },
- {
- title: "Requests",
- dataIndex: "requestsEvaluated",
- key: "requestsEvaluated",
- align: "right",
- sorter: true,
- sortOrder: sortBy === "requestsEvaluated" ? (sortDir === "desc" ? "descend" : "ascend") : null,
- render: (v: number) => v.toLocaleString(),
- },
- {
- title: "Fail Rate",
- dataIndex: "failRate",
- key: "failRate",
- align: "right",
- sorter: true,
- sortOrder: sortBy === "failRate" ? (sortDir === "desc" ? "descend" : "ascend") : null,
- render: (v: number, row) => (
- 15 ? "text-red-600" : v > 5 ? "text-amber-600" : "text-green-600"
- }
- >
- {v}%
- {row.trend === "up" && ↑}
- {row.trend === "down" && ↓}
-
- ),
- },
- {
- title: "Avg. latency added",
- dataIndex: "avgLatency",
- key: "avgLatency",
- align: "right",
- sorter: true,
- sortOrder: sortBy === "avgLatency" ? (sortDir === "desc" ? "descend" : "ascend") : null,
- render: (v?: number) => (
- 150 ? "text-red-600" : v > 50 ? "text-amber-600" : "text-green-600"
- }
- >
- {v != null ? `${v}ms` : "—"}
-
- ),
- },
- {
- title: "Status",
- dataIndex: "status",
- key: "status",
- align: "center",
- render: (status: string) => (
-
-
- {status}
-
- ),
- },
- ];
-
- const sortableKeys: SortKey[] = ["failRate", "requestsEvaluated", "avgLatency"];
- const handleTableChange = (_pagination: unknown, _filters: unknown, sorter: unknown) => {
- const s = sorter as { field?: keyof PerformanceRow; order?: string };
- if (s?.field && sortableKeys.includes(s.field as SortKey)) {
- setSortBy(s.field as SortKey);
- setSortDir(s.order === "ascend" ? "asc" : "desc");
+ const toggleSort = (key: SortKey) => {
+ if (sortBy === key) {
+ setSortDir(sortDir === "asc" ? "desc" : "asc");
+ } else {
+ setSortBy(key);
+ setSortDir("desc");
}
};
@@ -203,103 +139,222 @@ export function GuardrailsOverview({
-
-
Guardrails Monitor
+
+
+ Guardrails Monitor
+
-
+
Monitor guardrail performance across all requests
- } title="Coming soon">
+
-
-
-
-
-
- }
- />
-
-
- }
- />
-
-
- 150
- ? "text-red-600"
- : metrics.avgLatency > 50
- ? "text-amber-600"
- : "text-green-600"
- }
- />
-
-
-
-
-
+
+
+ }
+ />
+ }
+ />
+ 150
+ ? "text-red-600 dark:text-red-400"
+ : metrics.avgLatency > 50
+ ? "text-amber-600 dark:text-amber-400"
+ : "text-emerald-600 dark:text-emerald-400"
+ }
+ />
+
+
-
+
{(isLoading || error) && (
-
- {isLoading &&
}
- {error &&
Failed to load data. Try again.}
+
+ {isLoading && }
+ {error && (
+
+ Failed to load data. Try again.
+
+ )}
)}
-
+
-
+
Guardrail Performance
-
-
+
+
Click a guardrail to view details, logs, and configuration
}
+ variant="outline"
+ size="icon"
onClick={() => setEvaluationModalOpen(true)}
title="Evaluation settings"
- />
+ >
+
+
-
({
- onClick: () => onSelectGuardrail(row.id),
- style: { cursor: "pointer" },
- })}
- />
+
+
+
+ Guardrail
+ Provider
+ toggleSort("requestsEvaluated")}
+ >
+ Requests{" "}
+ {sortBy === "requestsEvaluated"
+ ? sortDir === "desc"
+ ? "↓"
+ : "↑"
+ : ""}
+
+ toggleSort("failRate")}
+ >
+ Fail Rate{" "}
+ {sortBy === "failRate" ? (sortDir === "desc" ? "↓" : "↑") : ""}
+
+ toggleSort("avgLatency")}
+ >
+ Avg. latency added{" "}
+ {sortBy === "avgLatency"
+ ? sortDir === "desc"
+ ? "↓"
+ : "↑"
+ : ""}
+
+ Status
+
+
+
+ {sorted.length === 0 && !isLoading ? (
+
+
+ No data for this period
+
+
+ ) : (
+ sorted.map((row) => (
+ onSelectGuardrail(row.id)}
+ >
+
+
+
+
+
+ {row.provider}
+
+
+
+ {row.requestsEvaluated.toLocaleString()}
+
+
+ 15
+ ? "text-red-600 dark:text-red-400"
+ : row.failRate > 5
+ ? "text-amber-600 dark:text-amber-400"
+ : "text-emerald-600 dark:text-emerald-400"
+ }
+ >
+ {row.failRate}%
+ {row.trend === "up" && (
+ ↑
+ )}
+ {row.trend === "down" && (
+ ↓
+ )}
+
+
+
+ 150
+ ? "text-red-600 dark:text-red-400"
+ : row.avgLatency > 50
+ ? "text-amber-600 dark:text-amber-400"
+ : "text-emerald-600 dark:text-emerald-400"
+ }
+ >
+ {row.avgLatency != null ? `${row.avgLatency}ms` : "—"}
+
+
+
+
+
+
+ {row.status}
+
+
+
+
+ ))
+ )}
+
+
= {
blocked: {
- icon: CloseOutlined,
- color: "text-red-600",
- bg: "bg-red-50",
- border: "border-red-200",
+ icon: X,
+ color: "text-red-600 dark:text-red-400",
+ bg: "bg-red-50 dark:bg-red-950/30",
+ border: "border-red-200 dark:border-red-800",
label: "Blocked",
},
passed: {
- icon: CheckCircleOutlined,
- color: "text-green-600",
- bg: "bg-green-50",
- border: "border-green-200",
+ icon: Check,
+ color: "text-emerald-600 dark:text-emerald-400",
+ bg: "bg-emerald-50 dark:bg-emerald-950/30",
+ border: "border-emerald-200 dark:border-emerald-800",
label: "Passed",
},
flagged: {
- icon: WarningOutlined,
- color: "text-amber-600",
- bg: "bg-amber-50",
- border: "border-amber-200",
+ icon: AlertTriangle,
+ color: "text-amber-600 dark:text-amber-400",
+ bg: "bg-amber-50 dark:bg-amber-950/30",
+ border: "border-amber-200 dark:border-amber-800",
label: "Flagged",
},
};
@@ -63,11 +70,13 @@ export function LogViewer({
}: LogViewerProps) {
const [sampleSize, setSampleSize] = useState(10);
const [activeFilter, setActiveFilter] = useState(filterAction);
- const [selectedRequestId, setSelectedRequestId] = useState(null);
+ const [selectedRequestId, setSelectedRequestId] = useState(
+ null,
+ );
const [drawerOpen, setDrawerOpen] = useState(false);
const filteredLogs = logs.filter(
- (log) => activeFilter === "all" || log.action === activeFilter
+ (log) => activeFilter === "all" || log.action === activeFilter,
);
const displayLogs = filteredLogs.slice(0, sampleSize);
const total = totalLogs ?? logs.length;
@@ -117,14 +126,14 @@ export function LogViewer({
};
return (
-
-
+
+
-
+
{guardrailName ? `Logs — ${guardrailName}` : "Request Logs"}
-
+
{logsLoading
? "Loading…"
: logs.length > 0
@@ -138,22 +147,24 @@ export function LogViewer({
{filters.map((f) => (
))}
-
+
-
Sample:
+
+ Sample:
+
{sampleSizes.map((size) => (