fix(guardrails/usage_endpoints.py): fix usage dashboard

This commit is contained in:
Krrish Dholakia 2026-02-21 19:33:50 -08:00
parent 886f1a3472
commit 9828b9929f
39 changed files with 118 additions and 198 deletions

View file

@ -106,9 +106,7 @@ def _aggregate_daily_metrics(metrics: Any, id_attr: str) -> Dict[str, Dict[str,
return agg
def _prev_fail_rates(
metrics_prev: Any, id_attr: str
) -> Dict[str, float]:
def _prev_fail_rates(metrics_prev: Any, id_attr: str) -> Dict[str, float]:
prev_agg_raw: Dict[str, Dict[str, int]] = {}
for m in metrics_prev:
gid = getattr(m, id_attr)
@ -137,15 +135,34 @@ def _chart_from_metrics(metrics: Any) -> List[Dict[str, Any]]:
]
def _get_guardrail_attrs(g: Any) -> tuple[Any, str]:
"""Get (guardrail_id, display_name) from guardrail - handles Prisma model or dict."""
gid = getattr(g, "guardrail_id", None) or (
g.get("guardrail_id") if isinstance(g, dict) else None
)
name = getattr(g, "guardrail_name", None) or (
g.get("guardrail_name") if isinstance(g, dict) else None
)
return gid, (name or gid or "")
def _guardrail_overview_rows(
guardrails: Any,
agg: Dict[str, Dict[str, Any]],
prev_agg: Dict[str, float],
) -> List[UsageOverviewRow]:
rows: List[UsageOverviewRow] = []
covered_keys: set = set()
for g in guardrails:
gid = g.guardrail_id
a = agg.get(gid, {"requests": 0, "passed": 0, "blocked": 0, "flagged": 0})
gid, display_name = _get_guardrail_attrs(g)
# Metrics are keyed by logical name from spend log metadata; guardrails table uses UUID
lookup_keys = [k for k in (display_name, gid) if k]
covered_keys.update(lookup_keys)
a = {"requests": 0, "passed": 0, "blocked": 0, "flagged": 0}
for k in lookup_keys:
if k in agg:
a = agg[k]
break
req, blocked = a["requests"], a["blocked"]
fail_rate = (100.0 * blocked / req) if req else 0.0
litellm_params = (
@ -156,16 +173,16 @@ def _guardrail_overview_rows(
(g.guardrail_info or {}) if isinstance(g.guardrail_info, dict) else {}
)
gtype = str(guardrail_info.get("type", "Guardrail"))
prev_fail = (
prev_agg.get(gid, 0.0)
if isinstance(prev_agg.get(gid), (int, float))
else 0.0
)
prev_fail = 0.0
for k in lookup_keys:
if k in prev_agg:
prev_fail = float(prev_agg.get(k, 0.0) or 0.0)
break
trend = _trend_from_comparison(fail_rate, prev_fail)
rows.append(
UsageOverviewRow(
id=gid,
name=g.guardrail_name or gid,
name=display_name or str(gid),
type=gtype,
provider=provider,
requestsEvaluated=req,
@ -176,6 +193,28 @@ def _guardrail_overview_rows(
trend=trend,
)
)
# Add rows for guardrails with metrics but not in guardrails table (e.g. MCP, config)
for agg_key, a in agg.items():
if agg_key in covered_keys or a["requests"] == 0:
continue
req, blocked = a["requests"], a["blocked"]
fail_rate = (100.0 * blocked / req) if req else 0.0
prev_fail = float(prev_agg.get(agg_key, 0.0) or 0.0)
trend = _trend_from_comparison(fail_rate, prev_fail)
rows.append(
UsageOverviewRow(
id=agg_key,
name=agg_key,
type="Guardrail",
provider="Custom",
requestsEvaluated=req,
failRate=round(fail_rate, 1),
avgScore=None,
avgLatency=None,
status=_status_from_fail_rate(fail_rate),
trend=trend,
)
)
return rows
@ -411,9 +450,9 @@ def _usage_log_entry_from_row(
duration = entry_for_guardrail.get("duration")
if duration is not None:
latency_val = round(float(duration) * 1000, 0)
score_val = entry_for_guardrail.get("confidence_score") or entry_for_guardrail.get(
"risk_score"
)
score_val = entry_for_guardrail.get(
"confidence_score"
) or entry_for_guardrail.get("risk_score")
if score_val is not None:
score_val = round(float(score_val), 2)
resp = entry_for_guardrail.get("guardrail_response")
@ -486,9 +525,7 @@ async def guardrails_usage_logs(
return UsageLogsResponse(logs=[], total=0, page=page, page_size=page_size)
try:
where = _build_usage_logs_where(
guardrail_id, policy_id, start_date, end_date
)
where = _build_usage_logs_where(guardrail_id, policy_id, start_date, end_date)
index_rows = await prisma_client.db.litellm_spendlogguardrailindex.find_many(
where=where,
order={"start_time": "desc"},

View file

@ -1,17 +1,14 @@
import {
ArrowLeftOutlined,
BellOutlined,
PlayCircleOutlined,
SafetyOutlined,
SettingOutlined,
WarningOutlined,
} from "@ant-design/icons";
import { useQuery } from "@tanstack/react-query";
import { Card, Col, Grid, Title } from "@tremor/react";
import { Col, Grid } from "@tremor/react";
import { Button, Spin, Tabs } from "antd";
import React, { useMemo, useState } from "react";
import {
formatDate,
getGuardrailsUsageDetail,
getGuardrailsUsageLogs,
} from "@/components/networking";
@ -24,6 +21,8 @@ interface GuardrailDetailProps {
guardrailId: string;
onBack: () => void;
accessToken?: string | null;
startDate: string;
endDate: string;
}
const statusColors: Record<
@ -35,19 +34,15 @@ const statusColors: Record<
critical: { bg: "bg-red-50", text: "text-red-700", dot: "bg-red-500" },
};
const defaultEnd = new Date();
const defaultStart = new Date();
defaultStart.setDate(defaultStart.getDate() - 7);
export function GuardrailDetail({
guardrailId,
onBack,
accessToken = null,
startDate,
endDate,
}: GuardrailDetailProps) {
const [activeTab, setActiveTab] = useState("overview");
const [evaluationModalOpen, setEvaluationModalOpen] = useState(false);
const [startDate] = useState(() => formatDate(defaultStart));
const [endDate] = useState(() => formatDate(defaultEnd));
const [logsPage, setLogsPage] = useState(1);
const logsPageSize = 50;
@ -156,23 +151,12 @@ export function GuardrailDetail({
<span className="inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-md bg-indigo-50 text-indigo-700 border border-indigo-200">
{data.provider}
</span>
<Button type="default" icon={<PlayCircleOutlined />} title="Coming soon">
Re-run AI
</Button>
<Button
type="default"
icon={<SettingOutlined />}
onClick={() => setEvaluationModalOpen(true)}
title="Evaluation settings"
/>
<Button
type="default"
icon={<BellOutlined />}
title="Coming soon"
className="opacity-75"
>
Notify
</Button>
</div>
</div>
</div>
@ -223,52 +207,6 @@ export function GuardrailDetail({
</Col>
</Grid>
<Card className="bg-white border border-gray-200 rounded-lg p-6">
<Title className="text-base font-semibold text-gray-900 mb-1">
Root Cause Analysis
</Title>
<p className="text-xs text-gray-500 mb-4">Common patterns in failing requests</p>
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 bg-red-50 rounded-lg border border-red-100">
<WarningOutlined className="text-red-500 mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-red-800">
High sensitivity to medical terminology
</p>
<p className="text-xs text-red-600 mt-0.5">
34% of blocked requests contain common medical terms (e.g., &quot;symptoms&quot;,
&quot;treatment&quot;, &quot;medication&quot;) that are benign in context.
Consider adding an allowlist or relaxing sensitivity for these categories.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-amber-50 rounded-lg border border-amber-100">
<WarningOutlined className="text-amber-500 mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-amber-800">
False positives on educational content
</p>
<p className="text-xs text-amber-600 mt-0.5">
22% of blocked requests are educational queries about safety topics. The guardrail
is flagging the topic itself rather than harmful intent.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
<WarningOutlined className="text-gray-400 mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-gray-800">
Sensitivity may be too aggressive
</p>
<p className="text-xs text-gray-600 mt-0.5">
Many blocked requests may be false positives. Consider relaxing sensitivity or
adding allowlisted patterns to reduce blocks by ~40% while maintaining safety.
</p>
</div>
</div>
</div>
</Card>
<LogViewer
guardrailName={data.name}
filterAction="blocked"

View file

@ -6,12 +6,10 @@ import * as networking from "@/components/networking";
vi.mock("@/components/networking", () => ({
getGuardrailsUsageOverview: vi.fn(),
getPoliciesUsageOverview: vi.fn(),
formatDate: vi.fn((d: Date) => d.toISOString().slice(0, 10)),
}));
const mockGetGuardrailsUsageOverview = vi.mocked(networking.getGuardrailsUsageOverview);
const mockGetPoliciesUsageOverview = vi.mocked(networking.getPoliciesUsageOverview);
function wrapper({ children }: { children: React.ReactNode }) {
const queryClient = new QueryClient({
@ -27,7 +25,7 @@ function wrapper({ children }: { children: React.ReactNode }) {
}
describe("GuardrailsMonitorView", () => {
it("should render overview and fetch guardrails and policies usage when accessToken is provided", async () => {
it("should render overview and fetch guardrails usage when accessToken is provided", async () => {
mockGetGuardrailsUsageOverview.mockResolvedValue({
rows: [],
chart: [],
@ -35,13 +33,6 @@ describe("GuardrailsMonitorView", () => {
totalBlocked: 0,
passRate: 100,
});
mockGetPoliciesUsageOverview.mockResolvedValue({
rows: [],
chart: [],
totalRequests: 0,
totalBlocked: 0,
passRate: 100,
});
render(
<GuardrailsMonitorView accessToken="test-token" />,
@ -51,7 +42,6 @@ describe("GuardrailsMonitorView", () => {
expect(await screen.findByRole("heading", { name: /Guardrails Monitor/i })).toBeDefined();
await waitFor(() => {
expect(mockGetGuardrailsUsageOverview).toHaveBeenCalled();
expect(mockGetPoliciesUsageOverview).toHaveBeenCalled();
});
});

View file

@ -1,6 +1,9 @@
import React, { useState } from "react";
import { GuardrailsOverview } from "./GuardrailsOverview";
import type { DateRangePickerValue } from "@tremor/react";
import React, { useCallback, useMemo, useState } from "react";
import { formatDate } from "@/components/networking";
import AdvancedDatePicker from "@/components/shared/advanced_date_picker";
import { GuardrailDetail } from "./GuardrailDetail";
import { GuardrailsOverview } from "./GuardrailsOverview";
type View =
| { type: "overview" }
@ -10,9 +13,28 @@ interface GuardrailsMonitorViewProps {
accessToken?: string | null;
}
const defaultEnd = new Date();
const defaultStart = new Date();
defaultStart.setDate(defaultStart.getDate() - 7);
export default function GuardrailsMonitorView({ accessToken = null }: GuardrailsMonitorViewProps) {
const [view, setView] = useState<View>({ type: "overview" });
const initialFrom = useMemo(() => new Date(defaultStart), []);
const initialTo = useMemo(() => new Date(defaultEnd), []);
const [dateValue, setDateValue] = useState<DateRangePickerValue>({
from: initialFrom,
to: initialTo,
});
const startDate = dateValue.from ? formatDate(dateValue.from) : "";
const endDate = dateValue.to ? formatDate(dateValue.to) : "";
const handleDateChange = useCallback((newValue: DateRangePickerValue) => {
setDateValue(newValue);
}, []);
const handleSelectGuardrail = (id: string) => {
setView({ type: "detail", guardrailId: id });
};
@ -23,13 +45,28 @@ export default function GuardrailsMonitorView({ accessToken = null }: Guardrails
return (
<div className="p-6 w-full min-w-0 flex-1">
<div className="flex items-center justify-end mb-4">
<AdvancedDatePicker
value={dateValue}
onValueChange={handleDateChange}
label=""
showTimeRange={false}
/>
</div>
{view.type === "overview" ? (
<GuardrailsOverview accessToken={accessToken} onSelectGuardrail={handleSelectGuardrail} />
<GuardrailsOverview
accessToken={accessToken}
startDate={startDate}
endDate={endDate}
onSelectGuardrail={handleSelectGuardrail}
/>
) : (
<GuardrailDetail
guardrailId={view.guardrailId}
onBack={handleBack}
accessToken={accessToken}
startDate={startDate}
endDate={endDate}
/>
)}
</div>

View file

@ -1,7 +1,5 @@
import {
DownloadOutlined,
FileTextOutlined,
PlayCircleOutlined,
RiseOutlined,
SafetyOutlined,
SettingOutlined,
@ -11,12 +9,8 @@ import { useQuery } from "@tanstack/react-query";
import { Card, Col, Grid, Title } from "@tremor/react";
import { Button, Spin, Table } from "antd";
import type { ColumnsType } from "antd/es/table";
import React, { useEffect, useMemo, useState } from "react";
import {
getGuardrailsUsageOverview,
getPoliciesUsageOverview,
} from "@/components/networking";
import { formatDate } from "@/components/networking";
import React, { useMemo, useState } from "react";
import { getGuardrailsUsageOverview } from "@/components/networking";
import { type PerformanceRow } from "./mockData";
import { EvaluationSettingsModal } from "./EvaluationSettingsModal";
import { MetricCard } from "./MetricCard";
@ -24,10 +18,11 @@ import { ScoreChart } from "./ScoreChart";
interface GuardrailsOverviewProps {
accessToken?: string | null;
startDate: string;
endDate: string;
onSelectGuardrail: (id: string) => void;
}
type ViewMode = "guardrails" | "policies";
type SortKey =
| "failRate"
| "requestsEvaluated"
@ -58,46 +53,25 @@ function computeMetricsFromRows(data: PerformanceRow[]) {
return { totalRequests, totalBlocked, passRate, avgLatency, count: data.length };
}
type RerunState = "idle" | "running" | "done";
const defaultEnd = new Date();
const defaultStart = new Date();
defaultStart.setDate(defaultStart.getDate() - 7);
export function GuardrailsOverview({
accessToken = null,
startDate,
endDate,
onSelectGuardrail,
}: GuardrailsOverviewProps) {
const [viewMode, setViewMode] = useState<ViewMode>("guardrails");
const [sortBy, setSortBy] = useState<SortKey>("failRate");
const [sortDir, setSortDir] = useState<"asc" | "desc">("desc");
const [rerunState, setRerunState] = useState<RerunState>("idle");
const [evaluationModalOpen, setEvaluationModalOpen] = useState(false);
const [startDate, setStartDate] = useState<string>(() => formatDate(defaultStart));
const [endDate, setEndDate] = useState<string>(() => formatDate(defaultEnd));
const { data: guardrailsData, isLoading: guardrailsLoading, error: guardrailsError } = useQuery({
queryKey: ["guardrails-usage-overview", startDate, endDate],
queryFn: () => getGuardrailsUsageOverview(accessToken!, startDate, endDate),
enabled: !!accessToken,
});
const { data: policiesData, isLoading: policiesLoading, error: policiesError } = useQuery({
queryKey: ["policies-usage-overview", startDate, endDate],
queryFn: () => getPoliciesUsageOverview(accessToken!, startDate, endDate),
enabled: !!accessToken,
});
useEffect(() => {
if (rerunState !== "done") return;
const t = setTimeout(() => setRerunState("idle"), 4000);
return () => clearTimeout(t);
}, [rerunState]);
const activeData: PerformanceRow[] = viewMode === "guardrails"
? (guardrailsData?.rows ?? [])
: (policiesData?.rows ?? []);
const activeData: PerformanceRow[] = guardrailsData?.rows ?? [];
const metrics = useMemo(() => {
if (viewMode === "guardrails" && guardrailsData) {
if (guardrailsData) {
return {
totalRequests: guardrailsData.totalRequests ?? 0,
totalBlocked: guardrailsData.totalBlocked ?? 0,
@ -106,18 +80,9 @@ export function GuardrailsOverview({
count: activeData.length,
};
}
if (viewMode === "policies" && policiesData) {
return {
totalRequests: policiesData.totalRequests ?? 0,
totalBlocked: policiesData.totalBlocked ?? 0,
passRate: String(policiesData.passRate ?? 0),
avgLatency: activeData.length ? Math.round(activeData.reduce((s, r) => s + (r.avgLatency ?? 0), 0) / activeData.length) : 0,
count: activeData.length,
};
}
return computeMetricsFromRows(activeData);
}, [viewMode, guardrailsData, policiesData, activeData]);
const chartData = viewMode === "guardrails" ? guardrailsData?.chart : policiesData?.chart;
}, [guardrailsData, activeData]);
const chartData = guardrailsData?.chart;
const sorted = useMemo(() => {
return [...activeData].sort((a, b) => {
const mult = sortDir === "desc" ? -1 : 1;
@ -126,14 +91,12 @@ export function GuardrailsOverview({
return (Number(aVal) - Number(bVal)) * mult;
});
}, [activeData, sortBy, sortDir]);
const isLoading = viewMode === "guardrails" ? guardrailsLoading : policiesLoading;
const error = viewMode === "guardrails" ? guardrailsError : policiesError;
const isGuardrails = viewMode === "guardrails";
const isLoading = guardrailsLoading;
const error = guardrailsError;
const columns: ColumnsType<PerformanceRow> = [
{
title: isGuardrails ? "Guardrail" : "Policy",
title: "Guardrail",
dataIndex: "name",
key: "name",
render: (name: string, row) => (
@ -236,12 +199,6 @@ export function GuardrailsOverview({
}
};
const handleRerun = () => {
if (rerunState !== "idle") return;
setRerunState("running");
setTimeout(() => setRerunState("done"), 2500);
};
return (
<div>
<div className="flex items-start justify-between mb-5">
@ -251,50 +208,16 @@ export function GuardrailsOverview({
<h1 className="text-xl font-semibold text-gray-900">Guardrails Monitor</h1>
</div>
<p className="text-sm text-gray-500">
{isGuardrails
? "Monitor guardrail performance across all requests"
: "Monitor policy enforcement across all requests"}
Monitor guardrail performance across all requests
</p>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-600 bg-white border border-gray-200 rounded-md px-3 py-2">
{startDate} – {endDate}
</span>
<Button type="default" icon={<DownloadOutlined />} title="Coming soon">
Export Data
</Button>
</div>
</div>
<div className="flex items-center gap-1 p-1 bg-gray-100 rounded-lg w-fit mb-6">
<button
type="button"
onClick={() => {
setViewMode("guardrails");
setSortBy("failRate");
setSortDir("desc");
}}
className={`inline-flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium rounded-md transition-colors ${
isGuardrails ? "bg-white text-gray-900 shadow-sm" : "text-gray-500 hover:text-gray-700"
}`}
>
<SafetyOutlined /> Guardrail Performance
</button>
<button
type="button"
onClick={() => {
setViewMode("policies");
setSortBy("failRate");
setSortDir("desc");
}}
className={`inline-flex items-center gap-1.5 px-3.5 py-1.5 text-sm font-medium rounded-md transition-colors ${
!isGuardrails ? "bg-white text-gray-900 shadow-sm" : "text-gray-500 hover:text-gray-700"
}`}
>
<FileTextOutlined /> Policy Performance
</button>
</div>
<Grid numItems={2} numItemsLg={5} className="gap-4 mb-6 items-stretch">
<Col className="flex flex-col">
<MetricCard label="Total Requests Evaluated" value={metrics.totalRequests.toLocaleString()} />
@ -330,7 +253,7 @@ export function GuardrailsOverview({
</Col>
<Col className="flex flex-col">
<MetricCard
label={isGuardrails ? "Active Guardrails" : "Active Policies"}
label="Active Guardrails"
value={metrics.count}
/>
</Col>
@ -350,12 +273,10 @@ export function GuardrailsOverview({
<div className="px-6 py-4 border-b border-gray-200 flex items-start justify-between gap-4">
<div>
<Title className="text-base font-semibold text-gray-900">
{isGuardrails ? "Guardrail Performance" : "Policy Performance"}
Guardrail Performance
</Title>
<p className="text-xs text-gray-500 mt-0.5">
{isGuardrails
? "Click a guardrail to view details, logs, and configuration"
: "Click a policy to view details, logs, and configuration"}
Click a guardrail to view details, logs, and configuration
</p>
</div>
<div className="flex items-center gap-2">
@ -365,9 +286,6 @@ export function GuardrailsOverview({
onClick={() => setEvaluationModalOpen(true)}
title="Evaluation settings"
/>
<Button type="default" icon={<PlayCircleOutlined />} title="Coming soon">
Re-run AI on last 100 logs
</Button>
</div>
</div>
<Table

View file

@ -16,7 +16,7 @@ export const pageDescriptions: Record<string, string> = {
"vector-stores": "Manage vector databases for embeddings",
new_usage: "View usage analytics and metrics",
logs: "Access request and response logs",
"guardrails-monitor": "Monitor guardrail and policy performance and view logs",
"guardrails-monitor": "Monitor guardrail performance and view logs",
users: "Manage internal user accounts and permissions",
teams: "Create and manage teams for access control",
organizations: "Manage organizations and their members",