diff --git a/litellm/proxy/guardrails/usage_endpoints.py b/litellm/proxy/guardrails/usage_endpoints.py index b5a578815f0..241f04b206d 100644 --- a/litellm/proxy/guardrails/usage_endpoints.py +++ b/litellm/proxy/guardrails/usage_endpoints.py @@ -343,11 +343,23 @@ async def guardrails_usage_detail( raise HTTPException(status_code=404, detail="Guardrail not found") + # Metrics are keyed by logical name (from spend log metadata), not UUID + logical_id = getattr(guardrail, "guardrail_name", None) or ( + guardrail.get("guardrail_name") if isinstance(guardrail, dict) else None + ) + metric_ids = [i for i in (logical_id, guardrail_id) if i] + metrics = await prisma_client.db.litellm_dailyguardrailmetrics.find_many( - where={"guardrail_id": guardrail_id, "date": {"gte": start, "lte": end}} + where={ + "guardrail_id": {"in": metric_ids}, + "date": {"gte": start, "lte": end}, + } ) metrics_prev = await prisma_client.db.litellm_dailyguardrailmetrics.find_many( - where={"guardrail_id": guardrail_id, "date": {"lt": start}} + where={ + "guardrail_id": {"in": metric_ids}, + "date": {"lt": start}, + } ) requests = sum(int(m.requests_evaluated or 0) for m in metrics) @@ -359,29 +371,41 @@ async def guardrails_usage_detail( prev_fail = (100.0 * prev_blocked / prev_req) if prev_req else 0.0 trend = _trend_from_comparison(fail_rate, prev_fail) + # Aggregate by date in case metrics exist under both UUID and logical name + ts_by_date: Dict[str, Dict[str, Any]] = {} + for m in metrics: + d = m.date + if d not in ts_by_date: + ts_by_date[d] = {"passed": 0, "blocked": 0} + ts_by_date[d]["passed"] += int(m.passed_count or 0) + ts_by_date[d]["blocked"] += int(m.blocked_count or 0) time_series = [ - { - "date": m.date, - "passed": int(m.passed_count or 0), - "blocked": int(m.blocked_count or 0), - "score": None, - } - for m in sorted(metrics, key=lambda x: x.date) + {"date": d, "passed": v["passed"], "blocked": v["blocked"], "score": None} + for d, v in sorted(ts_by_date.items()) ] + _litellm_params = getattr(guardrail, "litellm_params", None) or ( + guardrail.get("litellm_params") if isinstance(guardrail, dict) else None + ) litellm_params = ( - (guardrail.litellm_params or {}) - if isinstance(guardrail.litellm_params, dict) + _litellm_params + if isinstance(_litellm_params, dict) else {} ) + _guardrail_info = getattr(guardrail, "guardrail_info", None) or ( + guardrail.get("guardrail_info") if isinstance(guardrail, dict) else None + ) guardrail_info = ( - (guardrail.guardrail_info or {}) - if isinstance(guardrail.guardrail_info, dict) + _guardrail_info + if isinstance(_guardrail_info, dict) else {} ) + _guardrail_name = getattr(guardrail, "guardrail_name", None) or ( + guardrail.get("guardrail_name") if isinstance(guardrail, dict) else None + ) return UsageDetailResponse( guardrail_id=guardrail_id, - guardrail_name=guardrail.guardrail_name or guardrail_id, + guardrail_name=_guardrail_name or guardrail_id, type=str(guardrail_info.get("type", "Guardrail")), provider=str(litellm_params.get("guardrail", "Unknown")), requestsEvaluated=requests, @@ -525,7 +549,21 @@ 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) + # SpendLogGuardrailIndex stores logical names (guardrail_name) from metadata, not UUIDs. + # Resolve UUID to guardrail_name when querying from the dashboard (which passes guardrail_id). + effective_guardrail_id: Optional[str] = guardrail_id + if guardrail_id: + guardrail = await prisma_client.db.litellm_guardrailstable.find_unique( + where={"guardrail_id": guardrail_id} + ) + if guardrail: + logical_name = getattr(guardrail, "guardrail_name", None) + if logical_name: + effective_guardrail_id = logical_name + + where = _build_usage_logs_where( + effective_guardrail_id, policy_id, start_date, end_date + ) index_rows = await prisma_client.db.litellm_spendlogguardrailindex.find_many( where=where, order={"start_time": "desc"}, diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/GuardrailDetail.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/GuardrailDetail.tsx index ed9f435f9db..23aa52c8d3e 100644 --- a/ui/litellm-dashboard/src/components/GuardrailsMonitor/GuardrailDetail.tsx +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/GuardrailDetail.tsx @@ -213,6 +213,9 @@ export function GuardrailDetail({ logs={logs} logsLoading={logsLoading} totalLogs={logsData?.total ?? 0} + accessToken={accessToken} + startDate={startDate} + endDate={endDate} /> )} @@ -224,6 +227,9 @@ export function GuardrailDetail({ logs={logs} logsLoading={logsLoading} totalLogs={logsData?.total ?? 0} + accessToken={accessToken} + startDate={startDate} + endDate={endDate} /> )} diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx index f4af5f122e1..aed19ddfc8a 100644 --- a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx @@ -1,12 +1,16 @@ import { CheckCircleOutlined, CloseOutlined, - CopyOutlined, DownOutlined, WarningOutlined, } from "@ant-design/icons"; +import { useQuery } from "@tanstack/react-query"; +import moment from "moment"; import { Button, Spin } from "antd"; import React, { useState } from "react"; +import { uiSpendLogsCall } from "@/components/networking"; +import { LogDetailsDrawer } from "@/components/view_logs/LogDetailsDrawer"; +import type { LogEntry as ViewLogsLogEntry } from "@/components/view_logs/columns"; import type { LogEntry } from "./mockData"; const actionConfig: Record< @@ -42,6 +46,9 @@ interface LogViewerProps { logs?: LogEntry[]; logsLoading?: boolean; totalLogs?: number; + accessToken?: string | null; + startDate?: string; + endDate?: string; } export function LogViewer({ @@ -50,10 +57,14 @@ export function LogViewer({ logs = [], logsLoading = false, totalLogs, + accessToken = null, + startDate = "", + endDate = "", }: LogViewerProps) { const [sampleSize, setSampleSize] = useState(10); - const [expandedLog, setExpandedLog] = useState(null); const [activeFilter, setActiveFilter] = useState(filterAction); + const [selectedRequestId, setSelectedRequestId] = useState(null); + const [drawerOpen, setDrawerOpen] = useState(false); const filteredLogs = logs.filter( (log) => activeFilter === "all" || log.action === activeFilter @@ -68,6 +79,43 @@ export function LogViewer({ "passed", ]; + const startTime = startDate + ? moment(startDate).utc().format("YYYY-MM-DD HH:mm:ss") + : moment().subtract(24, "hours").utc().format("YYYY-MM-DD HH:mm:ss"); + const endTime = endDate + ? moment(endDate).utc().endOf("day").format("YYYY-MM-DD HH:mm:ss") + : moment().utc().format("YYYY-MM-DD HH:mm:ss"); + + const { data: fullLogResponse } = useQuery({ + queryKey: ["spend-log-by-request", selectedRequestId, startTime, endTime], + queryFn: async () => { + if (!accessToken || !selectedRequestId) return null; + const res = await uiSpendLogsCall({ + accessToken, + start_date: startTime, + end_date: endTime, + page: 1, + page_size: 10, + params: { request_id: selectedRequestId }, + }); + return res as { data: ViewLogsLogEntry[]; total: number }; + }, + enabled: Boolean(accessToken && selectedRequestId && drawerOpen), + }); + + const selectedLog: ViewLogsLogEntry | null = + fullLogResponse?.data?.[0] ?? null; + + const handleLogClick = (log: LogEntry) => { + setSelectedRequestId(log.id); + setDrawerOpen(true); + }; + + const handleCloseDrawer = () => { + setDrawerOpen(false); + setSelectedRequestId(null); + }; + return (
@@ -128,16 +176,15 @@ export function LogViewer({
)} {!logsLoading && displayLogs.length > 0 && ( -
- {displayLogs.map((log) => { - const config = actionConfig[log.action]; - const ActionIcon = config.icon; - const isExpanded = expandedLog === log.id; - return ( -
+
+ {displayLogs.map((log) => { + const config = actionConfig[log.action]; + const ActionIcon = config.icon; + return (
- - - + - - {isExpanded && ( -
-
-
-
- - Input - -
-

- {log.input_snippet ?? log.input ?? "—"} -

-
-
- - Output - -

- {log.output_snippet ?? log.output ?? "—"} -

-
- {(log.reason ?? log.score != null) && ( -
- - Reason - -

- {log.reason ?? (log.score != null ? `Score: ${log.score}` : "—")} -

-
- )} -
-
- )} -
- ); - })} -
+ ); + })} +
)} + + ); } diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx index 49ea447720c..1ab4744b893 100644 --- a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx @@ -78,7 +78,7 @@ const PROVIDERS_WITH_CUSTOM_RENDERERS = new Set([ ]); const formatMode = (mode: unknown): string => { - if (mode == null) return "—"; + if (mode == null || mode === "") return "—"; const s = typeof mode === "string" ? mode : String(mode); return s.replace(/_/g, "-").toUpperCase(); };