setExpanded(!expanded)}
+ >
+ {/* Status icon */}
+
+ {success ? : }
+
+
+ {/* Name + badges */}
+
+
{displayName}
+
+
+ {modeStr}
+
+
+
+ {success ? "PASSED" : "FAILED"}
+
+
+ {matchCountStr && (
+
+ {matchCountStr}
+
)}
-
- Mode:
- {entry.guardrail_mode}
-
-
- Status:
-
-
- {statusLabel}
+
+ {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()}
+
+ )}
+
- {/* Policy, detection method, confidence, patterns checked */}
-
+ {/* Expanded details */}
+ {expanded && (
+
+ {/* View Policy Configuration link */}
+ {entry.policy_template && (
+
+ )}
- {/* Classification details (LLM-judge) */}
- {entry.classification &&
}
+ {/* 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 && (
-
- )}
+ {/* Match details table */}
+ {entry.match_details && entry.match_details.length > 0 && (
+
+ )}
- {totalMaskedEntities > 0 && (
-
-
Masked Entity Summary
-
- {Object.entries(maskedEntityCount).map(([entityType, count]) => (
-
- {entityType}: {count}
-
- ))}
-
+ {/* 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 policyTemplates = Array.from(
- new Set(guardrailEntries.map((e) => e.policy_template).filter(Boolean))
- );
+ const totalOverheadMs = useMemo(() => {
+ return Math.round(guardrailEntries.reduce((sum, e) => sum + (e.duration ?? 0), 0) * 1000);
+ }, [guardrailEntries]);
- const tooltipTitle = allSucceeded ? null : "Guardrail failed to run.";
+ 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"}
-
- )}
-
- {policyTemplates.map((pt) => (
-
- {pt}
-
- ))}
+
+
+
+ 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}
+ ))}
);
}