setShowRaw(!showRaw)}
>
-
-
Raw Guardrail Response
+
+
Raw Guardrail Response
{showRaw && (
@@ -87,16 +265,162 @@ const GenericGuardrailResponse = ({ response }: { response: any }) => {
);
};
-const GuardrailDetails = ({ entry, index, total }: GuardrailDetailsProps) => {
- const guardrailProvider = entry.guardrail_provider ?? "presidio";
- const statusLabel = entry.guardrail_status ?? "unknown";
- const isSuccess = statusLabel.toLowerCase() === "success";
- const maskedEntityCount = entry.masked_entity_count || {};
- const totalMaskedEntities = Object.values(maskedEntityCount).reduce(
- (sum, count) => sum + (typeof count === "number" ? count : 0),
- 0,
+// ── Timeline entry types ────────────────────────────────────────────────────
+
+interface TimelineEntry {
+ type: "request" | "guardrail" | "llm" | "response";
+ label: string;
+ offsetMs: number;
+ status?: string;
+ isSuccess?: boolean;
+}
+
+const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
+ const sorted = useMemo(
+ () => [...entries].sort((a, b) => (a.start_time ?? 0) - (b.start_time ?? 0)),
+ [entries],
);
+ const timeline = useMemo(() => {
+ if (sorted.length === 0) return [];
+
+ const baseTime = sorted[0].start_time;
+ const items: TimelineEntry[] = [];
+
+ // Request received
+ items.push({ type: "request", label: "Request received", offsetMs: 0 });
+
+ // Pre-call guardrails
+ const preCalls = sorted.filter((e) => e.guardrail_mode === "pre_call");
+ const postCalls = sorted.filter((e) => e.guardrail_mode === "post_call" || e.guardrail_mode === "logging_only");
+ const duringCalls = sorted.filter((e) => e.guardrail_mode === "during_call");
+
+ for (const e of preCalls) {
+ const offsetMs = Math.round((e.end_time - baseTime) * 1000);
+ items.push({
+ type: "guardrail",
+ label: `Pre-call guardrail: ${getDisplayName(e)}`,
+ offsetMs,
+ status: isEntrySuccess(e) ? "PASSED" : "FAILED",
+ isSuccess: isEntrySuccess(e),
+ });
+ }
+
+ // LLM call — infer from gap between pre-call end and post-call start
+ const lastPreEnd = preCalls.length > 0 ? Math.max(...preCalls.map((e) => e.end_time)) : baseTime;
+ const firstPostStart = postCalls.length > 0 ? Math.min(...postCalls.map((e) => e.start_time)) : undefined;
+ const llmEndTime = firstPostStart ?? (lastPreEnd + 1);
+ const llmOffsetMs = Math.round((llmEndTime - baseTime) * 1000);
+
+ items.push({
+ type: "llm",
+ label: "LLM call",
+ offsetMs: llmOffsetMs,
+ });
+
+ // During-call guardrails (rare)
+ for (const e of duringCalls) {
+ const offsetMs = Math.round((e.end_time - baseTime) * 1000);
+ items.push({
+ type: "guardrail",
+ label: `During-call guardrail: ${getDisplayName(e)}`,
+ offsetMs,
+ status: isEntrySuccess(e) ? "PASSED" : "FAILED",
+ isSuccess: isEntrySuccess(e),
+ });
+ }
+
+ // Post-call guardrails
+ for (const e of postCalls) {
+ const offsetMs = Math.round((e.end_time - baseTime) * 1000);
+ items.push({
+ type: "guardrail",
+ label: `Post-call guardrail: ${getDisplayName(e)}`,
+ offsetMs,
+ status: isEntrySuccess(e) ? "PASSED" : "FAILED",
+ isSuccess: isEntrySuccess(e),
+ });
+ }
+
+ // Response returned
+ const maxEnd = Math.max(...sorted.map((e) => e.end_time));
+ const responseOffsetMs = Math.round((maxEnd - baseTime) * 1000) + 1;
+ items.push({ type: "response", label: "Response returned", offsetMs: responseOffsetMs });
+
+ return items;
+ }, [sorted]);
+
+ return (
+
+
+ Request Lifecycle
+
+
+ {timeline.map((item, idx) => (
+
+ {/* Vertical line */}
+
+
+ {item.type === "request" || item.type === "response" ? (
+
+ ) : item.type === "llm" ? (
+
+ ) : item.isSuccess ? (
+
+ ) : (
+
+ )}
+
+ {idx < timeline.length - 1 && (
+
+ )}
+
+
+ {/* Content */}
+
+
+
+ {item.label}
+
+ {item.status && (
+
+ {item.status}
+
+ )}
+
+ T+{item.offsetMs}ms
+
+
+
+
+ ))}
+
+
+ );
+};
+
+// ── Evaluation Card ─────────────────────────────────────────────────────────
+
+const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
+ const [expanded, setExpanded] = useState(false);
+ const success = isEntrySuccess(entry);
+ const totalMasked = getTotalMasked(entry);
+ const displayName = getDisplayName(entry);
+ const durationStr = formatDurationMs(entry.duration);
+ const modeStr = formatMode(entry.guardrail_mode);
+ const riskScore = getRiskScore(entry);
+
+ const guardrailProvider = entry.guardrail_provider ?? "presidio";
const guardrailResponse = entry.guardrail_response;
const presidioEntities = Array.isArray(guardrailResponse) ? guardrailResponse : [];
const bedrockResponse =
@@ -107,173 +431,287 @@ const GuardrailDetails = ({ entry, index, total }: GuardrailDetailsProps) => {
? (guardrailResponse as BedrockGuardrailResponse)
: undefined;
- return (
-
- {total > 1 && (
-
-
- Guardrail #{index + 1}
- {entry.guardrail_name}
-
-
- {guardrailProvider}
-
-
- )}
+ // Match count string: "X/Y matched" or "X matched"
+ const matchCountStr =
+ entry.patterns_checked != null
+ ? `${totalMasked}/${entry.patterns_checked} matched`
+ : totalMasked > 0
+ ? `${totalMasked} matched`
+ : null;
-
-
-
- Guardrail Name:
- {entry.guardrail_name}
-
-
- Mode:
- {entry.guardrail_mode}
-
-
-
Status:
-
-
- {statusLabel}
+ return (
+
+ {/* Collapsed header row */}
+
setExpanded(!expanded)}
+ >
+ {/* Status icon */}
+
+ {success ? : }
+
+
+ {/* Name + badges */}
+
+ {displayName}
+
+
+ {modeStr}
+
+
+
+ {success ? "PASSED" : "FAILED"}
+
+
+ {matchCountStr && (
+
+ {matchCountStr}
+
+ )}
+
+ {entry.confidence_score != null && (
+
+ {(entry.confidence_score * 100).toFixed(0)}% conf
+
+ )}
+
+ {riskScore != null && success && (
+
+
+ Risk {riskScore}/10
-
+ )}
-
-
- Start Time:
- {formatTime(entry.start_time)}
-
-
- End Time:
- {formatTime(entry.end_time)}
-
-
- Duration:
- {entry.duration.toFixed(4)}s
-
+ {/* Right side: duration + method + chevron */}
+
+ {durationStr}
+ {entry.detection_method && (
+
+ {entry.detection_method.split(",")[0].trim()}
+
+ )}
+
- {totalMaskedEntities > 0 && (
-
-
Masked Entity Summary
-
- {Object.entries(maskedEntityCount).map(([entityType, count]) => (
-
- {entityType}: {count}
-
- ))}
-
+ {/* Expanded details */}
+ {expanded && (
+
+ {/* View Policy Configuration link */}
+ {entry.policy_template && (
+
+ )}
+
+ {/* Classification details for llm-judge */}
+ {entry.classification && (
+
+
Classification
+ {entry.classification.category && (
+
+ Category:
+ {entry.classification.category}
+
+ )}
+ {entry.classification.article_reference && (
+
+ Reference:
+ {entry.classification.article_reference}
+
+ )}
+ {entry.classification.confidence != null && (
+
+ Confidence:
+ {(entry.classification.confidence * 100).toFixed(0)}%
+
+ )}
+ {entry.classification.reason && (
+
+ Reason:
+ {entry.classification.reason}
+
+ )}
+
+ )}
+
+ {/* Match details table */}
+ {entry.match_details && entry.match_details.length > 0 && (
+
+ )}
+
+ {/* Masked entity summary */}
+ {totalMasked > 0 && (
+
+
Masked Entities
+
+ {Object.entries(entry.masked_entity_count || {}).map(([entityType, count]) => (
+
+ {entityType}: {count}
+
+ ))}
+
+
+ )}
+
+ {/* Provider-specific details */}
+ {guardrailProvider === "presidio" && presidioEntities.length > 0 && (
+
+ )}
+ {guardrailProvider === "bedrock" && bedrockResponse && (
+
+
+
+ )}
+ {guardrailProvider === "litellm_content_filter" && guardrailResponse && (
+
+
+
+ )}
+ {guardrailProvider &&
+ !PROVIDERS_WITH_CUSTOM_RENDERERS.has(guardrailProvider) &&
+ guardrailResponse &&
}
)}
-
- {guardrailProvider === "presidio" && presidioEntities.length > 0 && (
-
- )}
-
- {guardrailProvider === "bedrock" && bedrockResponse && (
-
-
-
- )}
-
- {guardrailProvider === "litellm_content_filter" && guardrailResponse && (
-
-
-
- )}
-
- {/* Generic fallback for unknown guardrail providers */}
- {guardrailProvider &&
- !PROVIDERS_WITH_CUSTOM_RENDERERS.has(guardrailProvider) &&
- guardrailResponse &&
}
);
};
+// ── Main Component ──────────────────────────────────────────────────────────
+
const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
- const guardrailEntries = Array.isArray(data)
- ? data.filter((entry): entry is GuardrailInformation => Boolean(entry))
- : data
- ? [data]
- : [];
+ const guardrailEntries = useMemo(() => {
+ return Array.isArray(data)
+ ? data.filter((entry): entry is GuardrailInformation => Boolean(entry))
+ : data
+ ? [data]
+ : [];
+ }, [data]);
- const primaryName =
- guardrailEntries.length === 1 ? guardrailEntries[0].guardrail_name : `${guardrailEntries.length} guardrails`;
- const statuses = Array.from(new Set(guardrailEntries.map((entry) => entry.guardrail_status)));
- const allSucceeded = statuses.every((status) => (status ?? "").toLowerCase() === "success");
- const aggregatedStatus = allSucceeded ? "success" : "failure";
- const totalMaskedEntities = guardrailEntries.reduce((sum, entry) => {
- return (
- sum +
- Object.values(entry.masked_entity_count || {}).reduce(
- (acc, count) => acc + (typeof count === "number" ? count : 0),
- 0,
- )
- );
- }, 0);
+ const passedCount = guardrailEntries.filter(isEntrySuccess).length;
+ const allPassed = passedCount === guardrailEntries.length;
- const tooltipTitle = allSucceeded ? null : "Guardrail failed to run.";
+ const totalOverheadMs = useMemo(() => {
+ return Math.round(guardrailEntries.reduce((sum, e) => sum + (e.duration ?? 0), 0) * 1000);
+ }, [guardrailEntries]);
+
+ const policyTemplates = useMemo(() => {
+ return Array.from(new Set(guardrailEntries.map((e) => e.policy_template).filter(Boolean)));
+ }, [guardrailEntries]);
if (guardrailEntries.length === 0) {
return null;
}
+ const handleExport = () => {
+ const blob = new Blob([JSON.stringify(guardrailEntries, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `guardrail-compliance-log-${new Date().toISOString().slice(0, 10)}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
return (
-
-
- Guardrail Information
+
+ {/* ── Header ─────────────────────────────────────────────── */}
+
+
+
+
+
+ Guardrails & Policy Compliance
+
+
+
+ {guardrailEntries.length} guardrail{guardrailEntries.length !== 1 ? "s" : ""} evaluated
+
+
|
+
+ {allPassed ? (
+
+ ) : null}
+ {passedCount} Passed
+
+
+
+
-
-
- {aggregatedStatus}
-
-
-
-
{primaryName}
-
- {totalMaskedEntities > 0 && (
-
- {totalMaskedEntities} masked {totalMaskedEntities === 1 ? "entity" : "entities"}
-
- )}
+
+
+
+ Total: {totalOverheadMs}ms overhead
+
+ {policyTemplates.length > 0 && (
+
+ Policy: {policyTemplates.join(" / ")}
- ),
- children: (
-
- {guardrailEntries.map((entry, index) => (
-
- ))}
-
- ),
- },
- ]}
- />
+ )}
+
+
+
+
+
+
+ {/* ── Body: two columns ──────────────────────────────────── */}
+
+ {/* Left column: Request Lifecycle */}
+
+
+
+
+ {/* Right column: Evaluation Details */}
+
+
+ Evaluation Details
+
+
+ {guardrailEntries.map((entry, index) => (
+
+ ))}
+
+
+
);
};
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
index 9081219d5b..f0f4041531 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
@@ -66,6 +66,9 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails =
const hasGuardrailData = guardrailEntries.length > 0;
const totalMaskedEntities = calculateTotalMaskedEntities(guardrailEntries);
const primaryGuardrailLabel = getGuardrailLabel(guardrailEntries);
+ const guardrailPolicyNames = Array.from(
+ new Set(guardrailEntries.map((e: any) => e?.policy_template).filter(Boolean))
+ ) as string[];
// Vector store data
const hasVectorStoreData = checkHasVectorStoreData(metadata);
@@ -124,7 +127,7 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails =
)}
{hasGuardrailData && (
-
+
)}
@@ -164,7 +167,11 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails =
)}
{/* Guardrail Data */}
- {hasGuardrailData && }
+ {hasGuardrailData && (
+
+
+
+ )}
{/* Vector Store Data */}
{hasVectorStoreData && }
@@ -218,15 +225,23 @@ function TagsSection({ tags }: { tags: Record }) {
);
}
-function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: number }) {
+function GuardrailLabel({ label, maskedCount, policyNames }: { label: string; maskedCount: number; policyNames: string[] }) {
+ const handleClick = () => {
+ const el = document.getElementById("guardrail-section");
+ if (el) el.scrollIntoView({ behavior: "smooth" });
+ };
+
return (
- {label}
+ {label}
{maskedCount > 0 && (
{maskedCount} masked
)}
+ {policyNames.map((name) => (
+ {name}
+ ))}
);
}